Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc31acb80f | ||
|
|
1e5601a10b | ||
|
|
9f068b79a4 | ||
|
|
d23f736ead | ||
|
|
1ab656585b | ||
|
|
7cd682fe59 | ||
|
|
f523053fb3 | ||
|
|
d7bbbcae77 | ||
|
|
b1efaff34a | ||
|
|
7b35ed2939 | ||
|
|
07908ecaa1 | ||
|
|
d3857f7fde | ||
|
|
ecb4892bc8 | ||
|
|
2a2ce4bb92 | ||
|
|
6706b2b1ba | ||
|
|
c01fdcf012 | ||
|
|
2bd39fbd4e | ||
|
|
fa25a320eb | ||
|
|
80f7c8d571 | ||
|
|
2dc7768f5f | ||
|
|
2cf83149f0 | ||
|
|
077b40967c | ||
|
|
968b9d8b10 | ||
|
|
f130f583ba | ||
|
|
e126f340c6 | ||
|
|
ccf62aafce | ||
|
|
aa78c7bb6f | ||
|
|
65b355edd9 | ||
|
|
729aa9bc7f | ||
|
|
32821dabc7 | ||
|
|
026d7799f9 | ||
|
|
52bce4b925 | ||
|
|
7c29bc7ae5 | ||
|
|
0828630a5b | ||
|
|
ab5ce48f9c | ||
|
|
9656bbd332 | ||
|
|
a9316b9b8d | ||
|
|
0257beec38 | ||
|
|
1276ac0610 | ||
|
|
537897005c | ||
|
|
beb07fe007 | ||
|
|
644201dc8a | ||
|
|
ad46429aa9 | ||
|
|
71305e49f9 | ||
|
|
43af5f5bc8 | ||
|
|
01e6105240 | ||
|
|
2364b2311c | ||
|
|
8b4c25f475 | ||
|
|
b85cdb2596 | ||
|
|
497821bb41 | ||
|
|
9347e22843 | ||
|
|
3d4c937b71 | ||
|
|
ce8b539be3 | ||
|
|
53888c9dcb | ||
|
|
20a8078f3c | ||
|
|
24a6b7d513 | ||
|
|
ee0f90e247 | ||
|
|
d50a324e44 | ||
|
|
f2457817b2 | ||
|
|
7dd642b299 | ||
|
|
af5d9d2c0b | ||
|
|
f1a3753f36 | ||
|
|
7192fa5322 | ||
|
|
810e118059 | ||
|
|
48413ba344 | ||
|
|
81f1fa5cd7 | ||
|
|
7ee47e65bd | ||
|
|
688922808b | ||
|
|
ee4fd6d059 | ||
|
|
aba992d8a2 | ||
|
|
061e64fe98 | ||
|
|
7e035f1414 | ||
|
|
e8a6e4464a | ||
|
|
e986cccdd6 | ||
|
|
cb94aa042a | ||
|
|
1293176f89 | ||
|
|
057f4a4809 | ||
|
|
a3b2deec21 | ||
|
|
6c7ceac1a6 | ||
|
|
a46674844c | ||
|
|
ff9636b75b | ||
|
|
1be6b0681f |
@@ -0,0 +1,5 @@
|
||||
|
||||
SUPABASE_URL=https://supa.app-dev.hu
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzQwMjY1MjAwLAogICJleHAiOiAxODk4MDMxNjAwCn0.4cMVfAnBLxne1lq0fm94rgtXLBJdRx-0f-E4Jd_jFwI
|
||||
|
||||
GOOGLE_WEB_CLIENT_ID=34126164738-dejrjhuqrv1qehqf3pprkrs3jltrb89n.apps.googleusercontent.com
|
||||
@@ -0,0 +1,3 @@
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
GOOGLE_WEB_CLIENT_ID=
|
||||
@@ -69,6 +69,7 @@ unlinked_spec.ds
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/key.properties
|
||||
*.jks
|
||||
**/android/app/.cxx/
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
|
||||
Vendored
+15
-1
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"cmake.sourceDirectory": "${workspaceFolder}/linux/flutter"
|
||||
"cmake.sourceDirectory": "${workspaceFolder}/linux/flutter",
|
||||
"editor.wordBasedSuggestions": "off",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.selectionHighlight": false,
|
||||
"cSpell.words": [
|
||||
"azimut",
|
||||
"eleváció"
|
||||
],
|
||||
|
||||
"gitlens.remotes": [
|
||||
{
|
||||
"domain": "tea.app-dev.hu",
|
||||
"type": "Gitea"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,3 +11,7 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
+32
-48
@@ -1,52 +1,35 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id "com.google.gms.google-services"
|
||||
id "com.google.firebase.crashlytics"
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
def keystoreProperties=new Properties()
|
||||
def keystorePropertiesFile=rootProject.file('key.properties')
|
||||
if(keystorePropertiesFile.exists()){
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
ndkVersion "25.1.8937393"
|
||||
namespace = "hu.app_dev.terepi_seged"
|
||||
compileSdk 36
|
||||
// ndkVersion "25.1.8937393"
|
||||
// ndkVersion flutter.ndkVersion
|
||||
//ndkVersion "27.0.12077973"
|
||||
ndkVersion "28.2.13676358"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
// sourceCompatibility JavaVersion.VERSION_1_8
|
||||
// targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
// jvmTarget = JavaVersion.VERSION_1_8
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -54,11 +37,13 @@ android {
|
||||
applicationId "hu.app_dev.terepi_seged"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 35
|
||||
versionCode flutter.versionCode
|
||||
versionName flutter.versionName
|
||||
|
||||
// archivesBaseName = "terepi_seged-${versionName}-${new Date().format('yyyyMMdd-HHmm')}"
|
||||
// multiDexEnabled true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -83,8 +68,7 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation platform('com.google.firebase:firebase-bom:31.3.0')
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
}
|
||||
// dependencies {
|
||||
// implementation platform('com.google.firebase:firebase-bom:31.3.0')
|
||||
// implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
// }
|
||||
|
||||
@@ -13,6 +13,30 @@
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "34126164738-fkuskqkn4j9efh7c73ahm0olgkpqrsck.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "hu.app_dev.terepi_seged",
|
||||
"certificate_hash": "ce49d032c9782c11d0cde121f36751324846ff77"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "34126164738-oeb8993sbc9fqdoag66fi97fae1oun14.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "hu.app_dev.terepi_seged",
|
||||
"certificate_hash": "105d948b791a4b4bf3ab2489e230278bdcec4d3e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "34126164738-qb9m4ifm5sttbjo9km681kljsnmna8ds.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "hu.app_dev.terepi_seged",
|
||||
"certificate_hash": "f54d498b4e8f2d9c3b8d9f0d9a5a1d5f0b7e0297"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "34126164738-s3jjdhb3htkv6l18bqbgi53ebehhh4mp.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
android:name="${applicationName}"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="location"
|
||||
android:stopWithTask="false"/>
|
||||
<service
|
||||
android:name="com.baseflow.geolocator.GeolocatorLocationService"
|
||||
android:foregroundServiceType="location"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false"/>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -33,21 +44,35 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<data android:scheme="mailto"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> -->
|
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH" tools:remove="android:maxSdkVersion"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" tools:remove="android:maxSdkVersion"/> -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation" tools:targetApi="s"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
package hu.app_dev.terepi_seged
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val deviceInfoChannelName = "hu.app_dev.terepi_seged/deviceInfo"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine){
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
deviceInfoChannelName
|
||||
).setMethodCallHandler{call, result ->
|
||||
when(call.method){
|
||||
"getAndroidDeviceName"->{
|
||||
result.success(getAndroidDeviceName())
|
||||
}
|
||||
"getAndroidId"->{
|
||||
result.success(getAndroidId())
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getAndroidDeviceName(): String {
|
||||
val name = android.provider.Settings.Global.getString(
|
||||
contentResolver,
|
||||
android.provider.Settings.Global.DEVICE_NAME
|
||||
)
|
||||
|
||||
if (!name.isNullOrBlank()) {
|
||||
return name
|
||||
}
|
||||
|
||||
val manufacturer = android.os.Build.MANUFACTURER.orEmpty()
|
||||
val model = android.os.Build.MODEL.orEmpty()
|
||||
|
||||
return "$manufacturer $model".trim()
|
||||
}
|
||||
|
||||
private fun getAndroidId(): String? {
|
||||
return try {
|
||||
Settings.Secure.getString(
|
||||
contentResolver,
|
||||
Settings.Secure.ANDROID_ID
|
||||
)
|
||||
} catch (e: Exception){
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-14
@@ -1,17 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -25,6 +11,18 @@ allprojects {
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.hasProperty('android')) {
|
||||
project.android {
|
||||
if (namespace == null) {
|
||||
namespace project.group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
|
||||
+24
-8
@@ -1,11 +1,27 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.11.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
||||
id "com.google.gms.google-services" version "4.4.0" apply false
|
||||
id "com.google.firebase.crashlytics" version "2.9.9" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -24,6 +24,15 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>A nyomvonal rögzítéséhez folyamatos helymeghatározás szükséges.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Háttérben futó track rögzítéséhez szükséges.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class GeoidGrid {
|
||||
final double southLat, westLon, latStep, lonStep;
|
||||
final int nRows, nCols;
|
||||
final Float32List grid; // big-endian, south-to-north
|
||||
|
||||
GeoidGrid._(this.southLat, this.westLon, this.latStep, this.lonStep,
|
||||
this.nRows, this.nCols, this.grid);
|
||||
|
||||
static Future<GeoidGrid> load(String assetPath) async {
|
||||
final bytes = (await rootBundle.load(assetPath)).buffer.asByteData();
|
||||
final southLat = bytes.getFloat64(0, Endian.big);
|
||||
final westLon = bytes.getFloat64(8, Endian.big);
|
||||
final latStep = bytes.getFloat64(16, Endian.big);
|
||||
final lonStep = bytes.getFloat64(24, Endian.big);
|
||||
final nRows = bytes.getInt32(32, Endian.big);
|
||||
final nCols = bytes.getInt32(36, Endian.big);
|
||||
|
||||
final grid = Float32List(nRows * nCols);
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
grid[i] = bytes.getFloat32(40 + i * 4, Endian.big);
|
||||
}
|
||||
return GeoidGrid._(southLat, westLon, latStep, lonStep, nRows, nCols, grid);
|
||||
}
|
||||
|
||||
/// Returns EHT2014 geoid undulation [m] at (lat, lon) via bilinear interpolation.
|
||||
/// Returns null if outside grid or in no-data area.
|
||||
double? getUndulation(double lat, double lon) {
|
||||
final rowF = (lat - southLat) / latStep;
|
||||
final colF = (lon - westLon) / lonStep;
|
||||
final r0 = rowF.floor();
|
||||
final c0 = colF.floor();
|
||||
if (r0 < 0 || r0 >= nRows - 1 || c0 < 0 || c0 >= nCols - 1) return null;
|
||||
|
||||
final dr = rowF - r0;
|
||||
final dc = colF - c0;
|
||||
|
||||
double v(int r, int c) => grid[r * nCols + c].toDouble();
|
||||
final noData = -80.0;
|
||||
final v00 = v(r0, c0);
|
||||
if (v00 < noData) return null;
|
||||
final v10 = v(r0 + 1, c0);
|
||||
if (v10 < noData) return null;
|
||||
final v01 = v(r0, c0 + 1);
|
||||
if (v01 < noData) return null;
|
||||
final v11 = v(r0 + 1, c0 + 1);
|
||||
if (v11 < noData) return null;
|
||||
|
||||
return v00 * (1 - dr) * (1 - dc) +
|
||||
v10 * dr * (1 - dc) +
|
||||
v01 * (1 - dr) * dc +
|
||||
v11 * dr * dc;
|
||||
}
|
||||
|
||||
/// Orthometric height in the Hungarian EHT2014 system.
|
||||
/// [nmea_ortho] = GGA field 9 (altitudeAboveMeanSeaLevel)
|
||||
/// [nmea_sep] = GGA field 11 (geoidSeparation from receiver)
|
||||
double? toEovHeight(
|
||||
double lat, double lon, double nmeaOrtho, double nmeaSep) {
|
||||
final nEht2014 = getUndulation(lat, lon);
|
||||
if (nEht2014 == null) return null;
|
||||
final ellipsoidal = nmeaOrtho + nmeaSep; // h = H + N_receiver
|
||||
return ellipsoidal - nEht2014; // H_EOV = h - N_EHT2014
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
typedef MarkerCreationCallback = Marker Function(
|
||||
LatLng point, Map<String, dynamic> properties);
|
||||
typedef CircleMarkerCreationCallback = CircleMarker Function(
|
||||
LatLng point, Map<String, dynamic> properties);
|
||||
typedef PolylineCreationCallback = Polyline Function(
|
||||
List<LatLng> points, Map<String, dynamic> properties);
|
||||
typedef PolygonCreationCallback = Polygon Function(List<LatLng> points,
|
||||
List<List<LatLng>>? holePointsList, Map<String, dynamic> properties);
|
||||
typedef FilterFunction = bool Function(Map<String, dynamic> properties);
|
||||
|
||||
/// GeoJsonParser parses the GeoJson and fills three lists of parsed objects
|
||||
/// which are defined in flutter_map package
|
||||
/// - list of [Marker]s
|
||||
/// - list of [CircleMarker]s
|
||||
/// - list of [Polyline]s
|
||||
/// - list of [Polygon]s
|
||||
///
|
||||
/// One should pass these lists when creating adequate layers in flutter_map.
|
||||
/// For details see example.
|
||||
///
|
||||
/// Currently GeoJson parser supports only FeatureCollection and not GeometryCollection.
|
||||
/// See the GeoJson Format specification at: https://www.rfc-editor.org/rfc/rfc7946
|
||||
///
|
||||
/// For creation of [Marker], [Polyline], [CircleMarker] and [Polygon] objects the default callback functions
|
||||
/// are provided which are used in case when no user-defined callback function is provided.
|
||||
/// To fully customize the [Marker], [Polyline], [CircleMarker] and [Polygon] creation one has to write his own
|
||||
/// callback functions. As a template the default callback functions can be used.
|
||||
///
|
||||
class GeoJsonParser {
|
||||
/// list of [Marker] objects created as result of parsing
|
||||
final List<Marker> markers = [];
|
||||
|
||||
/// list of [Polyline] objects created as result of parsing
|
||||
final List<Polyline> polylines = [];
|
||||
|
||||
/// list of [Polygon] objects created as result of parsing
|
||||
final List<Polygon> polygons = [];
|
||||
|
||||
/// list of [CircleMarker] objects created as result of parsing
|
||||
final List<CircleMarker> circles = [];
|
||||
|
||||
/// user defined callback function that creates a [Marker] object
|
||||
MarkerCreationCallback? markerCreationCallback;
|
||||
|
||||
/// user defined callback function that creates a [Polyline] object
|
||||
PolylineCreationCallback? polyLineCreationCallback;
|
||||
|
||||
/// user defined callback function that creates a [Polygon] object
|
||||
PolygonCreationCallback? polygonCreationCallback;
|
||||
|
||||
/// user defined callback function that creates a [Polygon] object
|
||||
CircleMarkerCreationCallback? circleMarkerCreationCallback;
|
||||
|
||||
/// default [Marker] color
|
||||
Color? defaultMarkerColor;
|
||||
|
||||
/// default [Marker] icon
|
||||
IconData? defaultMarkerIcon;
|
||||
|
||||
/// default [Polyline] color
|
||||
Color? defaultPolylineColor;
|
||||
|
||||
/// default [Polyline] stroke
|
||||
double? defaultPolylineStroke;
|
||||
|
||||
/// default [Polygon] border color
|
||||
Color? defaultPolygonBorderColor;
|
||||
|
||||
/// default [Polygon] fill color
|
||||
Color? defaultPolygonFillColor;
|
||||
|
||||
/// default [Polygon] border stroke
|
||||
double? defaultPolygonBorderStroke;
|
||||
|
||||
/// default flag if [Polygon] is filled (default is true)
|
||||
bool? defaultPolygonIsFilled;
|
||||
|
||||
/// default [CircleMarker] border color
|
||||
Color? defaultCircleMarkerColor;
|
||||
|
||||
/// default [CircleMarker] border stroke
|
||||
Color? defaultCircleMarkerBorderColor;
|
||||
|
||||
/// default flag if [CircleMarker] is filled (default is true)
|
||||
bool? defaultCircleMarkerIsFilled;
|
||||
|
||||
/// user defined callback function called when the [Marker] is tapped
|
||||
void Function(Map<String, dynamic>)? onMarkerTapCallback;
|
||||
|
||||
/// user defined callback function called when the [CircleMarker] is tapped
|
||||
void Function(Map<String, dynamic>)? onCircleMarkerTapCallback;
|
||||
|
||||
/// user defined callback function called during parse for filtering
|
||||
FilterFunction? filterFunction;
|
||||
|
||||
/// default constructor - all parameters are optional and can be set later with setters
|
||||
GeoJsonParser({
|
||||
this.markerCreationCallback,
|
||||
this.polyLineCreationCallback,
|
||||
this.polygonCreationCallback,
|
||||
this.circleMarkerCreationCallback,
|
||||
this.filterFunction,
|
||||
this.defaultMarkerColor,
|
||||
this.defaultMarkerIcon,
|
||||
this.onMarkerTapCallback,
|
||||
this.defaultPolylineColor,
|
||||
this.defaultPolylineStroke,
|
||||
this.defaultPolygonBorderColor,
|
||||
this.defaultPolygonFillColor,
|
||||
this.defaultPolygonBorderStroke,
|
||||
this.defaultPolygonIsFilled,
|
||||
this.defaultCircleMarkerColor,
|
||||
this.defaultCircleMarkerBorderColor,
|
||||
this.defaultCircleMarkerIsFilled,
|
||||
this.onCircleMarkerTapCallback,
|
||||
});
|
||||
|
||||
/// parse GeJson in [String] format
|
||||
void parseGeoJsonAsString(String g) {
|
||||
return parseGeoJson(jsonDecode(g) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// set default [Marker] color
|
||||
set setDefaultMarkerColor(Color color) {
|
||||
defaultMarkerColor = color;
|
||||
}
|
||||
|
||||
/// set default [Marker] icon
|
||||
set setDefaultMarkerIcon(IconData ic) {
|
||||
defaultMarkerIcon = ic;
|
||||
}
|
||||
|
||||
/// set default [Marker] tap callback function
|
||||
void setDefaultMarkerTapCallback(
|
||||
Function(Map<String, dynamic> f) onTapFunction) {
|
||||
onMarkerTapCallback = onTapFunction;
|
||||
}
|
||||
|
||||
/// set default [CircleMarker] color
|
||||
set setDefaultCircleMarkerColor(Color color) {
|
||||
defaultCircleMarkerColor = color;
|
||||
}
|
||||
|
||||
/// set default [CircleMarker] tap callback function
|
||||
void setDefaultCircleMarkerTapCallback(
|
||||
Function(Map<String, dynamic> f) onTapFunction) {
|
||||
onCircleMarkerTapCallback = onTapFunction;
|
||||
}
|
||||
|
||||
/// set default [Polyline] color
|
||||
set setDefaultPolylineColor(Color color) {
|
||||
defaultPolylineColor = color;
|
||||
}
|
||||
|
||||
/// set default [Polyline] stroke
|
||||
set setDefaultPolylineStroke(double stroke) {
|
||||
defaultPolylineStroke = stroke;
|
||||
}
|
||||
|
||||
/// set default [Polygon] fill color
|
||||
set setDefaultPolygonFillColor(Color color) {
|
||||
defaultPolygonFillColor = color;
|
||||
}
|
||||
|
||||
/// set default [Polygon] border stroke
|
||||
set setDefaultPolygonBorderStroke(double stroke) {
|
||||
defaultPolygonBorderStroke = stroke;
|
||||
}
|
||||
|
||||
/// set default [Polygon] border color
|
||||
set setDefaultPolygonBorderColorStroke(Color color) {
|
||||
defaultPolygonBorderColor = color;
|
||||
}
|
||||
|
||||
/// set default [Polygon] setting whether polygon is filled
|
||||
set setDefaultPolygonIsFilled(bool filled) {
|
||||
defaultPolygonIsFilled = filled;
|
||||
}
|
||||
|
||||
/// main GeoJson parsing function
|
||||
void parseGeoJson(Map<String, dynamic> g) {
|
||||
// set default values if they are not specified by constructor
|
||||
markerCreationCallback ??= createDefaultMarker;
|
||||
circleMarkerCreationCallback ??= createDefaultCircleMarker;
|
||||
polyLineCreationCallback ??= createDefaultPolyline;
|
||||
polygonCreationCallback ??= createDefaultPolygon;
|
||||
filterFunction ??= defaultFilterFunction;
|
||||
defaultMarkerColor ??= Colors.red.withOpacity(0.8);
|
||||
defaultMarkerIcon ??= Icons.location_pin;
|
||||
defaultPolylineColor ??= Colors.blue.withOpacity(0.8);
|
||||
defaultPolylineStroke ??= 3.0;
|
||||
defaultPolygonBorderColor ??= Colors.black.withOpacity(0.8);
|
||||
defaultPolygonFillColor ??= Colors.black.withOpacity(0.1);
|
||||
defaultPolygonIsFilled ??= true;
|
||||
defaultPolygonBorderStroke ??= 1.0;
|
||||
defaultCircleMarkerColor ??= Colors.blue.withOpacity(0.25);
|
||||
defaultCircleMarkerBorderColor ??= Colors.black.withOpacity(0.8);
|
||||
defaultCircleMarkerIsFilled ??= true;
|
||||
|
||||
// loop through the GeoJson Map and parse it
|
||||
for (Map f in g['features'] as List) {
|
||||
String geometryType = f['geometry']['type'].toString();
|
||||
// check if this spatial object passes the filter function
|
||||
if (!filterFunction!(f['properties'] as Map<String, dynamic>)) {
|
||||
continue;
|
||||
}
|
||||
switch (geometryType) {
|
||||
case 'Point':
|
||||
{
|
||||
markers.add(
|
||||
markerCreationCallback!(
|
||||
LatLng(f['geometry']['coordinates'][1] as double,
|
||||
f['geometry']['coordinates'][0] as double),
|
||||
f['properties'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'Circle':
|
||||
{
|
||||
circles.add(
|
||||
circleMarkerCreationCallback!(
|
||||
LatLng(f['geometry']['coordinates'][1] as double,
|
||||
f['geometry']['coordinates'][0] as double),
|
||||
f['properties'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'MultiPoint':
|
||||
{
|
||||
for (final point in f['geometry']['coordinates'] as List) {
|
||||
markers.add(
|
||||
markerCreationCallback!(
|
||||
LatLng(point[1] as double, point[0] as double),
|
||||
f['properties'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'LineString':
|
||||
{
|
||||
final List<LatLng> lineString = [];
|
||||
for (final coords in f['geometry']['coordinates'] as List) {
|
||||
lineString.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
}
|
||||
polylines.add(polyLineCreationCallback!(
|
||||
lineString, f['properties'] as Map<String, dynamic>));
|
||||
}
|
||||
break;
|
||||
case 'MultiLineString':
|
||||
{
|
||||
for (final line in f['geometry']['coordinates'] as List) {
|
||||
final List<LatLng> lineString = [];
|
||||
for (final coords in line as List) {
|
||||
lineString
|
||||
.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
}
|
||||
polylines.add(polyLineCreationCallback!(
|
||||
lineString, f['properties'] as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Polygon':
|
||||
{
|
||||
final List<LatLng> outerRing = [];
|
||||
final List<List<LatLng>> holesList = [];
|
||||
int pathIndex = 0;
|
||||
for (final path in f['geometry']['coordinates'] as List) {
|
||||
final List<LatLng> hole = [];
|
||||
for (final coords in path as List<dynamic>) {
|
||||
if (pathIndex == 0) {
|
||||
// add to polygon's outer ring
|
||||
outerRing
|
||||
.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
} else {
|
||||
// add it to current hole
|
||||
hole.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
}
|
||||
}
|
||||
if (pathIndex > 0) {
|
||||
// add hole to the polygon's list of holes
|
||||
holesList.add(hole);
|
||||
}
|
||||
pathIndex++;
|
||||
}
|
||||
polygons.add(polygonCreationCallback!(
|
||||
outerRing, holesList, f['properties'] as Map<String, dynamic>));
|
||||
}
|
||||
break;
|
||||
case 'MultiPolygon':
|
||||
{
|
||||
for (final polygon in f['geometry']['coordinates'] as List) {
|
||||
final List<LatLng> outerRing = [];
|
||||
final List<List<LatLng>> holesList = [];
|
||||
int pathIndex = 0;
|
||||
for (final path in polygon as List) {
|
||||
List<LatLng> hole = [];
|
||||
for (final coords in path as List<dynamic>) {
|
||||
if (pathIndex == 0) {
|
||||
// add to polygon's outer ring
|
||||
outerRing
|
||||
.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
} else {
|
||||
// add it to a hole
|
||||
hole.add(LatLng(coords[1] as double, coords[0] as double));
|
||||
}
|
||||
}
|
||||
if (pathIndex > 0) {
|
||||
// add to polygon's list of holes
|
||||
holesList.add(hole);
|
||||
}
|
||||
pathIndex++;
|
||||
}
|
||||
polygons.add(polygonCreationCallback!(outerRing, holesList,
|
||||
f['properties'] as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/// default function for creating tappable [Marker]
|
||||
Widget defaultTappableMarker(Map<String, dynamic> properties,
|
||||
void Function(Map<String, dynamic>) onMarkerTap) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onMarkerTap(properties);
|
||||
},
|
||||
child: Icon(defaultMarkerIcon, color: defaultMarkerColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// default callback function for creating [Marker]
|
||||
Marker createDefaultMarker(LatLng point, Map<String, dynamic> properties) {
|
||||
return Marker(
|
||||
point: point,
|
||||
child: defaultTappableMarker(properties, markerTapped),
|
||||
);
|
||||
}
|
||||
|
||||
// /// default callback function for creating [Marker]
|
||||
// Marker createDefaultMarker(LatLng point, Map<String, dynamic> properties) {
|
||||
// return Marker(
|
||||
// point: point,
|
||||
// child: MouseRegion(
|
||||
// cursor: SystemMouseCursors.click,
|
||||
// child: GestureDetector(
|
||||
// onTap: () {
|
||||
// markerTapped(properties);
|
||||
// },
|
||||
// child: Icon(defaultMarkerIcon, color: defaultMarkerColor),
|
||||
// ),
|
||||
// ),
|
||||
// width: 60,
|
||||
// height: 60,
|
||||
// );
|
||||
// }
|
||||
|
||||
/// default callback function for creating [Polygon]
|
||||
CircleMarker createDefaultCircleMarker(
|
||||
LatLng point, Map<String, dynamic> properties) {
|
||||
return CircleMarker(
|
||||
point: point,
|
||||
radius: properties["radius"].toDouble(),
|
||||
useRadiusInMeter: true,
|
||||
color: defaultCircleMarkerColor!,
|
||||
borderColor: defaultCircleMarkerBorderColor!,
|
||||
);
|
||||
}
|
||||
|
||||
/// default callback function for creating [Polyline]
|
||||
Polyline createDefaultPolyline(
|
||||
List<LatLng> points, Map<String, dynamic> properties) {
|
||||
return Polyline(
|
||||
points: points,
|
||||
color: defaultPolylineColor!,
|
||||
strokeWidth: defaultPolylineStroke!);
|
||||
}
|
||||
|
||||
/// default callback function for creating [Polygon]
|
||||
Polygon createDefaultPolygon(List<LatLng> outerRing,
|
||||
List<List<LatLng>>? holesList, Map<String, dynamic> properties) {
|
||||
return Polygon(
|
||||
points: outerRing,
|
||||
holePointsList: holesList,
|
||||
borderColor: defaultPolygonBorderColor!,
|
||||
color: defaultPolygonFillColor!,
|
||||
// isFilled: defaultPolygonIsFilled!,
|
||||
borderStrokeWidth: defaultPolygonBorderStroke!,
|
||||
);
|
||||
}
|
||||
|
||||
/// the default filter function returns always true - therefore no filtering
|
||||
bool defaultFilterFunction(Map<String, dynamic> properties) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// default callback function called when tappable [Marker] is tapped
|
||||
void markerTapped(Map<String, dynamic> map) {
|
||||
if (onMarkerTapCallback != null) {
|
||||
onMarkerTapCallback!(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
enum Wgs84CoordinateFormat {
|
||||
decimalDegrees,
|
||||
degreesDecimalMinutes,
|
||||
degreesMinutesSeconds
|
||||
}
|
||||
|
||||
extension Wgs84CoordinateFormatLabel on Wgs84CoordinateFormat {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case Wgs84CoordinateFormat.decimalDegrees:
|
||||
return 'Tizedes fok';
|
||||
case Wgs84CoordinateFormat.degreesDecimalMinutes:
|
||||
return 'Fok + tizedes perc';
|
||||
case Wgs84CoordinateFormat.degreesMinutesSeconds:
|
||||
return 'Fok - perc - másodperc';
|
||||
}
|
||||
}
|
||||
|
||||
String get shortLabel {
|
||||
switch (this) {
|
||||
case Wgs84CoordinateFormat.decimalDegrees:
|
||||
return 'DD';
|
||||
case Wgs84CoordinateFormat.degreesDecimalMinutes:
|
||||
return 'DM';
|
||||
case Wgs84CoordinateFormat.degreesMinutesSeconds:
|
||||
return 'DMS';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Wgs84CoordinateFormatter {
|
||||
static String formatLatitude(
|
||||
double? value,
|
||||
Wgs84CoordinateFormat format,
|
||||
) {
|
||||
return _format(
|
||||
value: value,
|
||||
format: format,
|
||||
positiveHemisphere: 'N',
|
||||
negativeHemisphere: 'S',
|
||||
);
|
||||
}
|
||||
|
||||
static String formatLongitude(
|
||||
double? value,
|
||||
Wgs84CoordinateFormat format,
|
||||
) {
|
||||
return _format(
|
||||
value: value,
|
||||
format: format,
|
||||
positiveHemisphere: 'E',
|
||||
negativeHemisphere: 'W',
|
||||
);
|
||||
}
|
||||
|
||||
static String _format({
|
||||
required double? value,
|
||||
required Wgs84CoordinateFormat format,
|
||||
required String positiveHemisphere,
|
||||
required String negativeHemisphere,
|
||||
}) {
|
||||
if (value == null || value.isNaN || value.isInfinite) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
final hemisphere = value >= 0 ? positiveHemisphere : negativeHemisphere;
|
||||
final absValue = value.abs();
|
||||
|
||||
switch (format) {
|
||||
case Wgs84CoordinateFormat.decimalDegrees:
|
||||
return '${absValue.toStringAsFixed(8)}° $hemisphere';
|
||||
|
||||
case Wgs84CoordinateFormat.degreesDecimalMinutes:
|
||||
final deg = absValue.floor();
|
||||
final minutes = (absValue - deg) * 60.0;
|
||||
|
||||
return '$deg° ${minutes.toStringAsFixed(5)}′ $hemisphere';
|
||||
|
||||
case Wgs84CoordinateFormat.degreesMinutesSeconds:
|
||||
final deg = absValue.floor();
|
||||
final fullMinutes = (absValue - deg) * 60.0;
|
||||
final min = fullMinutes.floor();
|
||||
final sec = (fullMinutes - min) * 60.0;
|
||||
|
||||
return '$deg° $min′ ${sec.toStringAsFixed(3)}″ $hemisphere';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class GeometryMeasure {
|
||||
static final Distance _distance = const Distance();
|
||||
|
||||
static double lineLengthMeters(List<LatLng> points) {
|
||||
if (points.length < 2) return 0.0;
|
||||
|
||||
double sum = 0.0;
|
||||
|
||||
for (var i = 1; i < points.length; i++) {
|
||||
sum += _distance.as(
|
||||
LengthUnit.Meter,
|
||||
points[i - 1],
|
||||
points[i],
|
||||
);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
static double polygonAreaSquareMeters(List<LatLng> points) {
|
||||
final polygon = _normalizedPolygon(points);
|
||||
|
||||
if (polygon.length < 3) return 0.0;
|
||||
|
||||
final origin = _centroid(polygon);
|
||||
final projected =
|
||||
polygon.map((p) => _projectToLocalMeters(p, origin)).toList();
|
||||
|
||||
double area = 0.0;
|
||||
|
||||
for (var i = 0; i < projected.length; i++) {
|
||||
final j = (i + 1) % projected.length;
|
||||
|
||||
area += projected[i].x * projected[j].y - projected[j].x * projected[i].y;
|
||||
}
|
||||
|
||||
return area.abs() / 2.0;
|
||||
}
|
||||
|
||||
static List<LatLng> _normalizedPolygon(List<LatLng> source) {
|
||||
final points = List<LatLng>.from(source);
|
||||
|
||||
if (points.length < 2) return points;
|
||||
|
||||
final first = points.first;
|
||||
final last = points.last;
|
||||
|
||||
final closeDistance = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
first,
|
||||
last,
|
||||
);
|
||||
|
||||
if (closeDistance < 0.01) {
|
||||
points.removeLast();
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
static LatLng _centroid(List<LatLng> points) {
|
||||
double lat = 0.0;
|
||||
double lon = 0.0;
|
||||
|
||||
for (final p in points) {
|
||||
lat += p.latitude;
|
||||
lon += p.longitude;
|
||||
}
|
||||
|
||||
return LatLng(
|
||||
lat / points.length,
|
||||
lon / points.length,
|
||||
);
|
||||
}
|
||||
|
||||
static _Point2D _projectToLocalMeters(
|
||||
LatLng point,
|
||||
LatLng origin,
|
||||
) {
|
||||
const earthRadius = 6378137.0;
|
||||
|
||||
final latRad = point.latitude * pi / 180.0;
|
||||
final lonRad = point.longitude * pi / 180.0;
|
||||
|
||||
final originLatRad = origin.latitude * pi / 180.0;
|
||||
final originLonRad = origin.longitude * pi / 180.0;
|
||||
|
||||
final x = earthRadius * (lonRad - originLonRad) * cos(originLatRad);
|
||||
|
||||
final y = earthRadius * (latRad - originLatRad);
|
||||
|
||||
return _Point2D(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
class _Point2D {
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
const _Point2D(this.x, this.y);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
class GeometryMeasureFormatter {
|
||||
static String length(double meters) {
|
||||
if (meters < 1000.0) {
|
||||
return '${meters.toStringAsFixed(0)} m';
|
||||
}
|
||||
|
||||
return '${(meters / 1000.0).toStringAsFixed(2)} km';
|
||||
}
|
||||
|
||||
static String area(double squareMeters) {
|
||||
if (squareMeters < 10000.0) {
|
||||
return '${squareMeters.toStringAsFixed(0)} m²';
|
||||
}
|
||||
|
||||
return '${(squareMeters / 10000.0).toStringAsFixed(2)} ha';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
// GeoPackage (.gpkg) export — OGC szabvány, QGIS-kompatibilis
|
||||
//
|
||||
// Tartalom:
|
||||
// - note_items_point : rögzített pontok
|
||||
// - note_items_line : rögzített vonalak
|
||||
// - note_items_polygon : rögzített területek
|
||||
// - measured_points : bemért pontok
|
||||
// - tracks : GPS nyomvonalak
|
||||
//
|
||||
// Kimenet: ZIP archív (.gpkg + médiafájlok)
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:terepi_seged/models/measured_point.dart';
|
||||
import 'package:terepi_seged/services/app_database.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
|
||||
import '../models/note_item.dart';
|
||||
import '../enums/note_type.dart';
|
||||
|
||||
class GeoPackageExporter {
|
||||
// ── Publikus belépési pont ────────────────────────────────────────
|
||||
|
||||
Future<void> exportProject() async {
|
||||
final project = ProjectService.to.activeProject.value;
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
final name = project?.name ?? 'projekt';
|
||||
|
||||
final ts = DateFormat('yyyyMMdd_HHmm').format(DateTime.now());
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
final workDir = Directory(p.join(tmpDir.path, 'gpkg_export_$ts'));
|
||||
await workDir.create(recursive: true);
|
||||
|
||||
try {
|
||||
// 1. GeoPackage létrehozása
|
||||
final gpkgPath = p.join(workDir.path, '$name.gpkg');
|
||||
await _buildGpkg(gpkgPath, projectId);
|
||||
|
||||
// 2. Médiafájlok összegyűjtése
|
||||
final mediaDir = Directory(p.join(workDir.path, 'media'));
|
||||
await _collectMedia(projectId, mediaDir);
|
||||
|
||||
// 3. ZIP csomagolás
|
||||
final zipPath = p.join(tmpDir.path, '${name}_$ts.zip');
|
||||
await _packZip(workDir, zipPath);
|
||||
|
||||
// 4. Megosztás
|
||||
await SharePlus.instance.share(ShareParams(
|
||||
files: [XFile(zipPath, mimeType: 'application/zip')],
|
||||
subject: 'GeoPackage export — $name',
|
||||
));
|
||||
} finally {
|
||||
// Ideiglenes munkamappa törlése
|
||||
await workDir.delete(recursive: true).catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── GeoPackage (.gpkg) létrehozása ───────────────────────────────
|
||||
|
||||
Future<void> _buildGpkg(String path, int? projectId) async {
|
||||
final db = await openDatabase(path);
|
||||
try {
|
||||
await _initGpkg(db);
|
||||
await _exportNoteItems(db, projectId);
|
||||
await _exportMeasuredPoints(db, projectId);
|
||||
await _exportTracks(db, projectId);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ── GeoPackage kötelező táblák ────────────────────────────────────
|
||||
|
||||
Future<void> _initGpkg(Database db) async {
|
||||
// GeoPackage magic numbers
|
||||
await db.execute('PRAGMA application_id = 1196444487');
|
||||
await db.execute('PRAGMA user_version = 10200');
|
||||
|
||||
// Spatial reference systems
|
||||
await db.execute('''
|
||||
CREATE TABLE gpkg_spatial_ref_sys (
|
||||
srs_name TEXT NOT NULL,
|
||||
srs_id INTEGER NOT NULL PRIMARY KEY,
|
||||
organization TEXT NOT NULL,
|
||||
organization_coordsys_id INTEGER NOT NULL,
|
||||
definition TEXT NOT NULL,
|
||||
description TEXT
|
||||
)''');
|
||||
|
||||
// WGS84 bejegyzés
|
||||
await db.insert('gpkg_spatial_ref_sys', {
|
||||
'srs_name': 'WGS 84 geographic 2D',
|
||||
'srs_id': 4326,
|
||||
'organization': 'EPSG',
|
||||
'organization_coordsys_id': 4326,
|
||||
'definition': 'GEOGCS["WGS 84",DATUM["WGS_1984",'
|
||||
'SPHEROID["WGS 84",6378137,298.257223563]],'
|
||||
'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]',
|
||||
'description': 'longitude/latitude coordinates in decimal degrees',
|
||||
});
|
||||
// Undefined srs
|
||||
await db.insert('gpkg_spatial_ref_sys', {
|
||||
'srs_name': 'Undefined Cartesian SRS',
|
||||
'srs_id': -1,
|
||||
'organization': 'NONE',
|
||||
'organization_coordsys_id': -1,
|
||||
'definition': 'undefined',
|
||||
});
|
||||
await db.insert('gpkg_spatial_ref_sys', {
|
||||
'srs_name': 'Undefined geographic SRS',
|
||||
'srs_id': 0,
|
||||
'organization': 'NONE',
|
||||
'organization_coordsys_id': 0,
|
||||
'definition': 'undefined',
|
||||
});
|
||||
|
||||
// Contents tábla
|
||||
await db.execute('''
|
||||
CREATE TABLE gpkg_contents (
|
||||
table_name TEXT NOT NULL PRIMARY KEY,
|
||||
data_type TEXT NOT NULL,
|
||||
identifier TEXT,
|
||||
description TEXT DEFAULT '',
|
||||
last_change DATETIME NOT NULL
|
||||
DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', CURRENT_TIMESTAMP)),
|
||||
min_x REAL, min_y REAL, max_x REAL, max_y REAL,
|
||||
srs_id INTEGER,
|
||||
CONSTRAINT fk_gc_r_srs_id
|
||||
FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
|
||||
)''');
|
||||
|
||||
// Geometry columns tábla
|
||||
await db.execute('''
|
||||
CREATE TABLE gpkg_geometry_columns (
|
||||
table_name TEXT NOT NULL,
|
||||
column_name TEXT NOT NULL,
|
||||
geometry_type_name TEXT NOT NULL,
|
||||
srs_id INTEGER NOT NULL,
|
||||
z TINYINT NOT NULL,
|
||||
m TINYINT NOT NULL,
|
||||
CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),
|
||||
CONSTRAINT fk_gc_tn
|
||||
FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
|
||||
CONSTRAINT fk_gc_srs
|
||||
FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
|
||||
)''');
|
||||
}
|
||||
|
||||
// ── Feature tábla regisztrálása ───────────────────────────────────
|
||||
|
||||
Future<void> _registerLayer(
|
||||
Database db, {
|
||||
required String tableName,
|
||||
required String geomType, // 'POINT', 'LINESTRING', 'POLYGON'
|
||||
required String identifier,
|
||||
List<double>? extent, // [minX, minY, maxX, maxY]
|
||||
}) async {
|
||||
await db.insert('gpkg_contents', {
|
||||
'table_name': tableName,
|
||||
'data_type': 'features',
|
||||
'identifier': identifier,
|
||||
'description': '',
|
||||
'last_change': DateTime.now().toUtc().toIso8601String(),
|
||||
'min_x': extent?[0],
|
||||
'min_y': extent?[1],
|
||||
'max_x': extent?[2],
|
||||
'max_y': extent?[3],
|
||||
'srs_id': 4326,
|
||||
});
|
||||
await db.insert('gpkg_geometry_columns', {
|
||||
'table_name': tableName,
|
||||
'column_name': 'geom',
|
||||
'geometry_type_name': geomType,
|
||||
'srs_id': 4326,
|
||||
'z': 0,
|
||||
'm': 0,
|
||||
});
|
||||
}
|
||||
|
||||
// ── NoteItem exportok ─────────────────────────────────────────────
|
||||
|
||||
Future<void> _exportNoteItems(Database db, int? projectId) async {
|
||||
final items = await AppDatabase.instance.listNoteItems(projectId);
|
||||
final points = items.where((i) => i.type == NoteType.point).toList();
|
||||
final lines = items.where((i) => i.type == NoteType.line).toList();
|
||||
final polygons = items.where((i) => i.type == NoteType.polygon).toList();
|
||||
|
||||
if (points.isNotEmpty) await _exportPoints(db, points);
|
||||
if (lines.isNotEmpty) await _exportLines(db, lines);
|
||||
if (polygons.isNotEmpty) await _exportPolygons(db, polygons);
|
||||
}
|
||||
|
||||
Future<void> _exportPoints(Database db, List<NoteItem> items) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE note_items_point (
|
||||
id INTEGER PRIMARY KEY,
|
||||
geom BLOB NOT NULL,
|
||||
label TEXT,
|
||||
color_hex TEXT,
|
||||
created_at TEXT,
|
||||
photo_count INTEGER DEFAULT 0,
|
||||
audio_count INTEGER DEFAULT 0
|
||||
)''');
|
||||
await _registerLayer(db,
|
||||
tableName: 'note_items_point',
|
||||
geomType: 'POINT',
|
||||
identifier: 'Rögzített pontok',
|
||||
extent: _extent(items));
|
||||
|
||||
for (final item in items) {
|
||||
final pt = item.points.first;
|
||||
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
|
||||
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
|
||||
await db.insert('note_items_point', {
|
||||
'id': item.id,
|
||||
'geom': _gpkgPoint(pt.longitude, pt.latitude),
|
||||
'label': item.label,
|
||||
'color_hex':
|
||||
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
|
||||
'created_at': item.createdAt.toIso8601String(),
|
||||
'photo_count': pc,
|
||||
'audio_count': ac,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportLines(Database db, List<NoteItem> items) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE note_items_line (
|
||||
id INTEGER PRIMARY KEY,
|
||||
geom BLOB NOT NULL,
|
||||
label TEXT,
|
||||
color_hex TEXT,
|
||||
stroke_width REAL,
|
||||
point_count INTEGER,
|
||||
created_at TEXT,
|
||||
photo_count INTEGER DEFAULT 0,
|
||||
audio_count INTEGER DEFAULT 0
|
||||
)''');
|
||||
await _registerLayer(db,
|
||||
tableName: 'note_items_line',
|
||||
geomType: 'LINESTRING',
|
||||
identifier: 'Rögzített vonalak',
|
||||
extent: _extent(items));
|
||||
|
||||
for (final item in items) {
|
||||
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
|
||||
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
|
||||
await db.insert('note_items_line', {
|
||||
'id': item.id,
|
||||
'geom': _gpkgLineString(item.points),
|
||||
'label': item.label,
|
||||
'color_hex':
|
||||
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
|
||||
'stroke_width': item.strokeWidth,
|
||||
'point_count': item.points.length,
|
||||
'created_at': item.createdAt.toIso8601String(),
|
||||
'photo_count': pc,
|
||||
'audio_count': ac,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportPolygons(Database db, List<NoteItem> items) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE note_items_polygon (
|
||||
id INTEGER PRIMARY KEY,
|
||||
geom BLOB NOT NULL,
|
||||
label TEXT,
|
||||
color_hex TEXT,
|
||||
opacity REAL,
|
||||
stroke_width REAL,
|
||||
point_count INTEGER,
|
||||
created_at TEXT,
|
||||
photo_count INTEGER DEFAULT 0,
|
||||
audio_count INTEGER DEFAULT 0
|
||||
)''');
|
||||
await _registerLayer(db,
|
||||
tableName: 'note_items_polygon',
|
||||
geomType: 'POLYGON',
|
||||
identifier: 'Rögzített területek',
|
||||
extent: _extent(items));
|
||||
|
||||
for (final item in items) {
|
||||
final pc = await AppDatabase.instance.countNotePhotos(item.id!);
|
||||
final ac = await AppDatabase.instance.countNoteAudios(item.id!);
|
||||
await db.insert('note_items_polygon', {
|
||||
'id': item.id,
|
||||
'geom': _gpkgPolygon(item.points),
|
||||
'label': item.label,
|
||||
'color_hex':
|
||||
'#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
|
||||
'opacity': item.opacity,
|
||||
'stroke_width': item.strokeWidth,
|
||||
'point_count': item.points.length,
|
||||
'created_at': item.createdAt.toIso8601String(),
|
||||
'photo_count': pc,
|
||||
'audio_count': ac,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bemért pontok exportja ────────────────────────────────────────
|
||||
|
||||
Future<void> _exportMeasuredPoints(Database db, int? projectId) async {
|
||||
final points = projectId != null
|
||||
? await AppDatabase.instance.listMeasuredPoints(projectId)
|
||||
: <MeasuredPoint>[];
|
||||
if (points.isEmpty) return;
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE measured_points (
|
||||
id INTEGER PRIMARY KEY,
|
||||
geom BLOB NOT NULL,
|
||||
name TEXT,
|
||||
eov_y REAL,
|
||||
eov_x REAL,
|
||||
eov_z REAL,
|
||||
accuracy REAL,
|
||||
fix_quality INTEGER,
|
||||
timestamp TEXT,
|
||||
note TEXT
|
||||
)''');
|
||||
await _registerLayer(db,
|
||||
tableName: 'measured_points',
|
||||
geomType: 'POINT',
|
||||
identifier: 'Bemért pontok');
|
||||
|
||||
for (final pt in points) {
|
||||
if (pt.latitude == null || pt.longitude == null) continue;
|
||||
await db.insert('measured_points', {
|
||||
'id': pt.id,
|
||||
'geom': _gpkgPoint(pt.longitude!, pt.latitude!),
|
||||
'name': pt.name,
|
||||
'eov_y': pt.eovY,
|
||||
'eov_x': pt.eovX,
|
||||
'eov_z': pt.eovZ,
|
||||
'accuracy': pt.accuracy,
|
||||
'fix_quality': pt.fixQuality,
|
||||
'timestamp': pt.timestamp.toIso8601String(),
|
||||
'note': pt.note,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Track exportja ────────────────────────────────────────────────
|
||||
|
||||
Future<void> _exportTracks(Database db, int? projectId) async {
|
||||
final tracks = await AppDatabase.instance.listTracks();
|
||||
final filtered = projectId != null
|
||||
? tracks.where((t) => t.projectId == projectId).toList()
|
||||
: tracks;
|
||||
if (filtered.isEmpty) return;
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE tracks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
geom BLOB NOT NULL,
|
||||
name TEXT,
|
||||
distance_meters REAL,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
point_count INTEGER
|
||||
)''');
|
||||
await _registerLayer(db,
|
||||
tableName: 'tracks',
|
||||
geomType: 'LINESTRING',
|
||||
identifier: 'GPS nyomvonalak');
|
||||
|
||||
for (final track in filtered) {
|
||||
final pts = await AppDatabase.instance.getLatLons(track.id!);
|
||||
if (pts.length < 2) continue;
|
||||
|
||||
// Egyszerű record list → LatLng-szerű párok
|
||||
final coords = pts.map((p) => (lon: p.lon, lat: p.lat)).toList();
|
||||
await db.insert('tracks', {
|
||||
'id': track.id,
|
||||
'geom': _gpkgLineStringFromRecords(coords),
|
||||
'name': track.name,
|
||||
'distance_meters': track.distanceMeters,
|
||||
'start_time': track.startTime.toIso8601String(),
|
||||
'end_time': track.endTime?.toIso8601String(),
|
||||
'point_count': pts.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Médiafájlok másolása ──────────────────────────────────────────
|
||||
|
||||
Future<void> _collectMedia(int? projectId, Directory mediaDir) async {
|
||||
if (projectId == null) return;
|
||||
|
||||
final photos = Directory(p.join(mediaDir.path, 'photos'));
|
||||
final audios = Directory(p.join(mediaDir.path, 'audio'));
|
||||
await photos.create(recursive: true);
|
||||
await audios.create(recursive: true);
|
||||
|
||||
final items = await AppDatabase.instance.listNoteItems(projectId);
|
||||
for (final item in items) {
|
||||
// Fotók
|
||||
final photoList = await AppDatabase.instance.listNotePhotos(item.id!);
|
||||
for (final photo in photoList) {
|
||||
final src = File(photo.localPath);
|
||||
if (await src.exists()) {
|
||||
final dst = File(p.join(photos.path, p.basename(photo.localPath)));
|
||||
await src.copy(dst.path);
|
||||
}
|
||||
}
|
||||
// Hangok
|
||||
final audioList = await AppDatabase.instance.listNoteAudios(item.id!);
|
||||
for (final audio in audioList) {
|
||||
final src = File(audio.localPath);
|
||||
if (await src.exists()) {
|
||||
final dst = File(p.join(audios.path, p.basename(audio.localPath)));
|
||||
await src.copy(dst.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ZIP csomagolás ────────────────────────────────────────────────
|
||||
|
||||
Future<void> _packZip(Directory workDir, String zipPath) async {
|
||||
final archive = Archive();
|
||||
|
||||
for (final entity in workDir.listSync(recursive: true)) {
|
||||
if (entity is! File) continue;
|
||||
final bytes = entity.readAsBytesSync();
|
||||
final rel = p.relative(entity.path, from: workDir.path);
|
||||
final archiveName = rel.replaceAll('\\', '/'); // Windows path fix
|
||||
archive.addFile(ArchiveFile(archiveName, bytes.length, bytes));
|
||||
}
|
||||
|
||||
final encoded = ZipEncoder().encode(archive);
|
||||
if (encoded == null || encoded.isEmpty) {
|
||||
throw Exception(
|
||||
'ZIP encoding sikertelen — nincsenek exportálható adatok');
|
||||
}
|
||||
await File(zipPath).writeAsBytes(Uint8List.fromList(encoded));
|
||||
}
|
||||
|
||||
// ── WKB/GeoPackage bináris kódolás ───────────────────────────────
|
||||
//
|
||||
// GPKG binary header (8 byte):
|
||||
// 0x47 0x50 – 'GP' magic
|
||||
// 0x00 – version 0
|
||||
// 0x01 – flags: little-endian, no envelope
|
||||
// srs_id (int32) – 4326 (WGS84)
|
||||
// + WKB geometry
|
||||
|
||||
Uint8List _gpkgPoint(double lon, double lat) {
|
||||
final b = ByteData(8 + 21); // header + wkb point
|
||||
_writeGpkgHeader(b, 0);
|
||||
b.setUint8(8, 1); // WKB: little endian
|
||||
b.setUint32(9, 1, Endian.little); // type: Point
|
||||
b.setFloat64(13, lon, Endian.little);
|
||||
b.setFloat64(21, lat, Endian.little);
|
||||
return b.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Uint8List _gpkgLineString(List<dynamic> points) {
|
||||
final n = points.length;
|
||||
final b = ByteData(8 + 9 + n * 16);
|
||||
_writeGpkgHeader(b, 0);
|
||||
int o = 8;
|
||||
b.setUint8(o++, 1);
|
||||
b.setUint32(o, 2, Endian.little);
|
||||
o += 4; // type: LineString
|
||||
b.setUint32(o, n, Endian.little);
|
||||
o += 4;
|
||||
for (final pt in points) {
|
||||
b.setFloat64(o, pt.longitude, Endian.little);
|
||||
o += 8;
|
||||
b.setFloat64(o, pt.latitude, Endian.little);
|
||||
o += 8;
|
||||
}
|
||||
return b.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Uint8List _gpkgLineStringFromRecords(
|
||||
List<({double lon, double lat})> coords) {
|
||||
final n = coords.length;
|
||||
final b = ByteData(8 + 9 + n * 16);
|
||||
_writeGpkgHeader(b, 0);
|
||||
int o = 8;
|
||||
b.setUint8(o++, 1);
|
||||
b.setUint32(o, 2, Endian.little);
|
||||
o += 4;
|
||||
b.setUint32(o, n, Endian.little);
|
||||
o += 4;
|
||||
for (final c in coords) {
|
||||
b.setFloat64(o, c.lon, Endian.little);
|
||||
o += 8;
|
||||
b.setFloat64(o, c.lat, Endian.little);
|
||||
o += 8;
|
||||
}
|
||||
return b.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Uint8List _gpkgPolygon(List<dynamic> ring) {
|
||||
final n = ring.length;
|
||||
final b = ByteData(8 + 9 + 4 + n * 16);
|
||||
_writeGpkgHeader(b, 0);
|
||||
int o = 8;
|
||||
b.setUint8(o++, 1);
|
||||
b.setUint32(o, 3, Endian.little);
|
||||
o += 4; // type: Polygon
|
||||
b.setUint32(o, 1, Endian.little);
|
||||
o += 4; // 1 ring
|
||||
b.setUint32(o, n, Endian.little);
|
||||
o += 4;
|
||||
for (final pt in ring) {
|
||||
b.setFloat64(o, pt.longitude, Endian.little);
|
||||
o += 8;
|
||||
b.setFloat64(o, pt.latitude, Endian.little);
|
||||
o += 8;
|
||||
}
|
||||
return b.buffer.asUint8List();
|
||||
}
|
||||
|
||||
void _writeGpkgHeader(ByteData b, int offset) {
|
||||
b.setUint8(offset, 0x47); // 'G'
|
||||
b.setUint8(offset + 1, 0x50); // 'P'
|
||||
b.setUint8(offset + 2, 0x00); // version
|
||||
b.setUint8(offset + 3, 0x01); // flags: little-endian, no envelope
|
||||
b.setInt32(offset + 4, 4326, Endian.little); // WGS84
|
||||
}
|
||||
|
||||
// ── Segédek ───────────────────────────────────────────────────────
|
||||
|
||||
/// Befoglaló téglalap az összes NoteItem pontjaiból
|
||||
List<double>? _extent(List<NoteItem> items) {
|
||||
final pts = items.expand((i) => i.points).toList();
|
||||
if (pts.isEmpty) return null;
|
||||
return [
|
||||
pts.map((p) => p.longitude).reduce((a, b) => a < b ? a : b),
|
||||
pts.map((p) => p.latitude).reduce((a, b) => a < b ? a : b),
|
||||
pts.map((p) => p.longitude).reduce((a, b) => a > b ? a : b),
|
||||
pts.map((p) => p.latitude).reduce((a, b) => a > b ? a : b),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// KML és KMZ → flutter_map objektumok
|
||||
//
|
||||
// FONTOS: KML koordináta sorrend: lon,lat,alt
|
||||
// Flutter LatLng(lat, lon) — tehát fordítva kell olvasni!
|
||||
//
|
||||
// KMZ = ZIP archív, benne doc.kml
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../models/imported_layer.dart';
|
||||
|
||||
class KmlParser {
|
||||
/// KML szöveg dekódolása a fájlban DEKLARÁLT kódolással (vagy UTF-8,
|
||||
/// ha nincs deklaráció — ez az XML alapértelmezése).
|
||||
///
|
||||
/// * UTF-8 deklaráció (vagy annak hiánya) → UTF-8 dekódolás; ha ez
|
||||
/// mégis érvénytelen bájtsorozatra futna (rosszul címkézett fájl),
|
||||
/// Latin-1-re esünk vissza — az minden bájt-értékre ad valamilyen
|
||||
/// karaktert, nem dob kivételt.
|
||||
/// * Explicit ISO-8859-1 / Windows-1252 deklaráció → Latin-1.
|
||||
/// * Egyéb, Dart alapkönyvtára által NEM ismert kódolás (pl.
|
||||
/// ISO-8859-2, Windows-1250 — ezek adnák helyesen az ő/ű betűket
|
||||
/// egy esetleges nem-UTF-8 exportnál) → egyértelmű hibaüzenet,
|
||||
/// ahelyett hogy csendben félrement szöveget adnánk.
|
||||
static String decodeText(List<int> bytes) {
|
||||
final declared = _detectDeclaredEncoding(bytes);
|
||||
|
||||
if (declared == null ||
|
||||
declared.contains('utf-8') ||
|
||||
declared.contains('utf8')) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} catch (_) {
|
||||
return latin1.decode(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
if (declared.contains('8859-1') ||
|
||||
declared.contains('latin1') ||
|
||||
declared.contains('windows-1252') ||
|
||||
declared.contains('cp1252')) {
|
||||
return latin1.decode(bytes);
|
||||
}
|
||||
|
||||
if (declared == 'ascii' || declared == 'us-ascii') {
|
||||
return ascii.decode(bytes, allowInvalid: true);
|
||||
}
|
||||
|
||||
throw FormatException(
|
||||
'Ismeretlen vagy nem támogatott KML kódolás a fájlban: '
|
||||
'"$declared". Exportáld a fájlt UTF-8 kódolással.');
|
||||
}
|
||||
|
||||
static String? _detectDeclaredEncoding(List<int> bytes) {
|
||||
final headLen = bytes.length < 200 ? bytes.length : 200;
|
||||
final head = ascii.decode(bytes.sublist(0, headLen), allowInvalid: true);
|
||||
final match =
|
||||
RegExp(r'''encoding\s*=\s*["']([^"']+)["']''').firstMatch(head);
|
||||
return match?.group(1)?.toLowerCase();
|
||||
}
|
||||
|
||||
// Alapértelmezett stílusok — felülírhatók konstruktorban
|
||||
final Color defaultPolylineColor;
|
||||
final double defaultPolylineStroke;
|
||||
final Color defaultPolygonFillColor;
|
||||
final Color defaultPolygonBorderColor;
|
||||
final double defaultPolygonBorderStroke;
|
||||
final Color defaultMarkerColor;
|
||||
|
||||
const KmlParser({
|
||||
this.defaultPolylineColor = const Color(0xCC1565C0), // kék
|
||||
this.defaultPolylineStroke = 3.0,
|
||||
this.defaultPolygonFillColor = const Color(0x4D1565C0),
|
||||
this.defaultPolygonBorderColor = const Color(0xCC1565C0),
|
||||
this.defaultPolygonBorderStroke = 1.5,
|
||||
this.defaultMarkerColor = Colors.red,
|
||||
});
|
||||
|
||||
// ── KMZ (ZIP → KML) ──────────────────────────────────────────────
|
||||
|
||||
ImportedLayer parseKmz(Uint8List bytes, String fileName,
|
||||
{String? id, bool isVisible = true}) {
|
||||
final archive = ZipDecoder().decodeBytes(bytes);
|
||||
|
||||
ArchiveFile? kmlFile = archive.findFile('doc.kml');
|
||||
kmlFile ??= archive.files.firstWhere(
|
||||
(f) => f.name.toLowerCase().endsWith('.kml'),
|
||||
orElse: () => throw Exception('Nem található .kml fájl a KMZ-ben'),
|
||||
);
|
||||
|
||||
//final kmlString = String.fromCharCodes(kmlFile.content as List<int>);
|
||||
final kmlString = utf8.decode(kmlFile.content as List<int>);
|
||||
return _parse(kmlString, fileName, LayerImportSourceType.kmz,
|
||||
id: id, isVisible: isVisible);
|
||||
}
|
||||
|
||||
// ── KML (szöveg) ──────────────────────────────────────────────────
|
||||
|
||||
ImportedLayer parseKml(String kmlString, String fileName,
|
||||
{String? id, bool isVisible = true}) =>
|
||||
_parse(kmlString, fileName, LayerImportSourceType.kml,
|
||||
id: id, isVisible: isVisible);
|
||||
|
||||
// ── Belső parse ───────────────────────────────────────────────────
|
||||
|
||||
ImportedLayer _parse(
|
||||
String kmlString, String fileName, LayerImportSourceType source,
|
||||
{String? id, bool isVisible = true}) {
|
||||
final doc = XmlDocument.parse(kmlString);
|
||||
final markers = <Marker>[];
|
||||
final polylines = <Polyline>[];
|
||||
final polygons = <Polygon>[];
|
||||
|
||||
for (final pm in doc.findAllElements('Placemark')) {
|
||||
final style = _style(pm);
|
||||
final label = pm.findElements('name').firstOrNull?.innerText.trim() ?? '';
|
||||
|
||||
// Point
|
||||
final point = pm.findElements('Point').firstOrNull;
|
||||
if (point != null) {
|
||||
final pt = _singleCoord(point);
|
||||
if (pt != null) {
|
||||
markers.add(Marker(
|
||||
point: pt,
|
||||
width: label.isNotEmpty ? 120 : 24,
|
||||
height: label.isNotEmpty ? 40 : 24,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _markerWidget(label, style.lineColor),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// LineString
|
||||
final line = pm.findElements('LineString').firstOrNull;
|
||||
if (line != null) {
|
||||
final pts = _coordList(line);
|
||||
if (pts.isNotEmpty) {
|
||||
polylines.add(Polyline(
|
||||
points: pts,
|
||||
color: style.lineColor,
|
||||
strokeWidth: style.lineWidth,
|
||||
borderColor: style.lineColor.withOpacity(0.3),
|
||||
borderStrokeWidth: 1.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
final poly = pm.findElements('Polygon').firstOrNull;
|
||||
if (poly != null) {
|
||||
final rings = _polygonRings(poly);
|
||||
if (rings.isNotEmpty) {
|
||||
polygons.add(Polygon(
|
||||
points: rings[0],
|
||||
holePointsList: rings.length > 1 ? rings.sublist(1) : null,
|
||||
color: style.fillColor,
|
||||
borderColor: style.lineColor,
|
||||
borderStrokeWidth: style.lineWidth,
|
||||
label: label.isNotEmpty ? label : null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// MultiGeometry — rekurzív feldolgozás az első egyszerű elemre
|
||||
final multi = pm.findElements('MultiGeometry').firstOrNull;
|
||||
if (multi != null) {
|
||||
for (final ls in multi.findElements('LineString')) {
|
||||
final pts = _coordList(ls);
|
||||
if (pts.isNotEmpty) {
|
||||
polylines.add(Polyline(
|
||||
points: pts,
|
||||
color: style.lineColor,
|
||||
strokeWidth: style.lineWidth,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ImportedLayer(
|
||||
id: id ?? const Uuid().v4(),
|
||||
isVisible: isVisible,
|
||||
name: fileName,
|
||||
sourceType: source,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
polygons: polygons,
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Koordináta parserek ───────────────────────────────────────────
|
||||
|
||||
LatLng? _singleCoord(XmlElement geom) {
|
||||
final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim();
|
||||
if (raw == null) return null;
|
||||
return _oneCoord(raw.split(RegExp(r'\s+')).first);
|
||||
}
|
||||
|
||||
List<LatLng> _coordList(XmlElement geom) {
|
||||
final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim();
|
||||
if (raw == null) return [];
|
||||
return raw
|
||||
.split(RegExp(r'\s+'))
|
||||
.map(_oneCoord)
|
||||
.whereType<LatLng>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<List<LatLng>> _polygonRings(XmlElement polygon) {
|
||||
final rings = <List<LatLng>>[];
|
||||
final outer = polygon
|
||||
.findElements('outerBoundaryIs')
|
||||
.firstOrNull
|
||||
?.findElements('LinearRing')
|
||||
.firstOrNull;
|
||||
if (outer != null) rings.add(_coordList(outer));
|
||||
for (final inner in polygon.findElements('innerBoundaryIs')) {
|
||||
final ring = inner.findElements('LinearRing').firstOrNull;
|
||||
if (ring != null) rings.add(_coordList(ring));
|
||||
}
|
||||
return rings.where((r) => r.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
/// KML: lon,lat,alt → LatLng(lat, lon)
|
||||
LatLng? _oneCoord(String s) {
|
||||
final p = s.split(',');
|
||||
if (p.length < 2) return null;
|
||||
try {
|
||||
return LatLng(
|
||||
double.parse(p[1].trim()), // lat
|
||||
double.parse(p[0].trim()), // lon
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stílus kiolvasás ──────────────────────────────────────────────
|
||||
|
||||
_Style _style(XmlElement pm) {
|
||||
final style = pm.findElements('Style').firstOrNull;
|
||||
if (style == null) return _Style.defaults(this);
|
||||
|
||||
Color lineColor = defaultPolylineColor;
|
||||
double lineWidth = defaultPolylineStroke;
|
||||
Color fillColor = defaultPolygonFillColor;
|
||||
|
||||
final ls = style.findElements('LineStyle').firstOrNull;
|
||||
if (ls != null) {
|
||||
final c = ls.findElements('color').firstOrNull?.innerText;
|
||||
if (c != null) lineColor = _kmlColor(c);
|
||||
final w = ls.findElements('width').firstOrNull?.innerText;
|
||||
if (w != null) lineWidth = double.tryParse(w) ?? lineWidth;
|
||||
}
|
||||
|
||||
final ps = style.findElements('PolyStyle').firstOrNull;
|
||||
if (ps != null) {
|
||||
final c = ps.findElements('color').firstOrNull?.innerText;
|
||||
if (c != null) fillColor = _kmlColor(c);
|
||||
}
|
||||
|
||||
return _Style(
|
||||
lineColor: lineColor, lineWidth: lineWidth, fillColor: fillColor);
|
||||
}
|
||||
|
||||
/// KML szín formátum: aabbggrr → Flutter Color(argb)
|
||||
Color _kmlColor(String s) {
|
||||
if (s.length != 8) return defaultPolylineColor;
|
||||
try {
|
||||
final a = int.parse(s.substring(0, 2), radix: 16);
|
||||
final b = int.parse(s.substring(2, 4), radix: 16);
|
||||
final g = int.parse(s.substring(4, 6), radix: 16);
|
||||
final r = int.parse(s.substring(6, 8), radix: 16);
|
||||
return Color.fromARGB(a, r, g, b);
|
||||
} catch (_) {
|
||||
return defaultPolylineColor;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Marker widget ─────────────────────────────────────────────────
|
||||
|
||||
Widget _markerWidget(String label, Color color) {
|
||||
if (label.isEmpty) {
|
||||
return Icon(Icons.location_pin, color: color, size: 24);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
decoration:
|
||||
BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
Icon(Icons.location_pin, color: color, size: 16),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _Style {
|
||||
final Color lineColor;
|
||||
final double lineWidth;
|
||||
final Color fillColor;
|
||||
const _Style(
|
||||
{required this.lineColor,
|
||||
required this.lineWidth,
|
||||
required this.fillColor});
|
||||
factory _Style.defaults(KmlParser p) => _Style(
|
||||
lineColor: p.defaultPolylineColor,
|
||||
lineWidth: p.defaultPolylineStroke,
|
||||
fillColor: p.defaultPolygonFillColor,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// Közös interfész — MapSurveyController és LayerStyleSession is implementálja.
|
||||
/// A ColorRow, OpacitySlider, StrokeSlider ezt várja (nem a teljes controllert).
|
||||
abstract class StyleEditable {
|
||||
Rx<Color> get activeEditColor;
|
||||
RxDouble get activeEditOpacity;
|
||||
RxDouble get activeEditStrokeWidth;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum LayerImportSourceType { geoJson, kml, kmz }
|
||||
@@ -0,0 +1,6 @@
|
||||
enum MapEditTool {
|
||||
none,
|
||||
point,
|
||||
line,
|
||||
polygon,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum LayerType { geojson, shapefile, dxf, csv, postgis, wms, track, gnss }
|
||||
@@ -0,0 +1 @@
|
||||
enum LayerVisibility { visible, hidden }
|
||||
@@ -0,0 +1 @@
|
||||
enum MapMeasureType { none, distance, area }
|
||||
@@ -0,0 +1 @@
|
||||
enum MapSurveyMode { browse, measure, stakeout, fieldWalk, track }
|
||||
@@ -0,0 +1 @@
|
||||
enum NoteType { point, line, polygon }
|
||||
@@ -1,35 +1,55 @@
|
||||
import 'package:nmea/nmea.dart';
|
||||
|
||||
class Gngga extends TalkerSentence {
|
||||
static const String id = "GNGGA";
|
||||
static const String id = "GxGGA";
|
||||
Gngga({required super.raw});
|
||||
|
||||
@override
|
||||
bool get valid => super.valid && fields.length == 15;
|
||||
bool get valid => super.valid && fields.length >= 15;
|
||||
|
||||
String get utcOfPositionFix => fields[1];
|
||||
int get gpsQualityIndicator => int.parse(fields[6]);
|
||||
double get latitude => _getCoordinateFromGpsLatitude(fields[2]);
|
||||
String get latitudeDirection => fields[3];
|
||||
double get longitude => _getCoordinateFromGpsLongitude(fields[4]);
|
||||
String get longitudeDirection => fields[5];
|
||||
int get numberOfSvsInUse => int.parse(fields[7]);
|
||||
double get hdop => double.parse(fields[8]);
|
||||
double get altitudeAboveMeanSeaLevel => double.parse(fields[9]);
|
||||
double get geoidSeparation => double.parse(fields[11]);
|
||||
String get utcOfPositionFix => _field(1);
|
||||
int get gpsQualityIndicator => _parseInt(_field(6), fallback: 0);
|
||||
String get latitudeDirection => _field(3).toUpperCase();
|
||||
double get latitude => _parseLatitude(_field(2), latitudeDirection) ?? 0.0;
|
||||
String get longitudeDirection => _field(5).toUpperCase();
|
||||
double get longitude => _parseLongitude(_field(4), longitudeDirection) ?? 0.0;
|
||||
int get numberOfSvsInUse => _parseInt(_field(7), fallback: 0);
|
||||
double get hdop => _parseDouble(_field(8), fallback: 0.0);
|
||||
double get altitudeAboveMeanSeaLevel =>
|
||||
_parseDouble(_field(9), fallback: 0.0);
|
||||
double get geoidSeparation => _parseDouble(_field(11), fallback: 0.0);
|
||||
|
||||
double _getCoordinateFromGpsLongitude(String coordinate) {
|
||||
double result =
|
||||
double.parse("${coordinate[0]}${coordinate[1]}${coordinate[2]}") +
|
||||
double.parse(coordinate.substring(3)) / 60;
|
||||
String _field(int index) {
|
||||
if (index < 0 || index >= fields.length) return '';
|
||||
|
||||
return result;
|
||||
return fields[index].trim();
|
||||
}
|
||||
|
||||
double _getCoordinateFromGpsLatitude(String coordinate) {
|
||||
double result = double.parse("${coordinate[0]}${coordinate[1]}") +
|
||||
double.parse(coordinate.substring(2)) / 60;
|
||||
int _parseInt(String value, {required int fallback}) {
|
||||
return int.tryParse(value) ?? fallback;
|
||||
}
|
||||
|
||||
return result;
|
||||
double _parseDouble(String value, {required double fallback}) {
|
||||
return double.tryParse(value) ?? fallback;
|
||||
}
|
||||
|
||||
double? _parseLatitude(String value, String direction) {
|
||||
if (value.length < 4) return null; // ddmm.mmmm
|
||||
final deg = double.tryParse(value.substring(0, 2));
|
||||
final min = double.tryParse(value.substring(2));
|
||||
if (deg == null || min == null) return null;
|
||||
|
||||
final result = deg + (min / 60.0);
|
||||
return direction == 'S' ? -result : result;
|
||||
}
|
||||
|
||||
double? _parseLongitude(String value, String direction) {
|
||||
if (value.length < 5) return null; // dddmm.mmmm
|
||||
final deg = double.tryParse(value.substring(0, 3));
|
||||
final min = double.tryParse(value.substring(3));
|
||||
if (deg == null || min == null) return null;
|
||||
|
||||
final result = deg + (min / 60.0);
|
||||
return direction == 'W' ? -result : result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:nmea/nmea.dart';
|
||||
import 'package:terepi_seged/models/satellite_info.dart';
|
||||
|
||||
/// GSA — GNSS DOP és aktív műholdak
|
||||
///
|
||||
/// Formátum:
|
||||
/// $GNGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,2.1,1.2,1.7*XX
|
||||
/// │ │ └─────────── 12 db PRN ────────┘ │ │ │
|
||||
/// │ └── Fix típus: 1=nincs, 2=2D, 3=3D │ │ └─ VDOP
|
||||
/// └──── Mód: M=kézi, A=auto │ └───── HDOP
|
||||
/// └───────── PDOP
|
||||
///
|
||||
/// NMEA 4.1+: opcionális rendszer azonosító (fields[18])
|
||||
/// Minden rendszerhez érkezik egy-egy GSA mondat.
|
||||
class Gngsa extends TalkerSentence {
|
||||
static const String id = 'GNGSA';
|
||||
|
||||
Gngsa({required super.raw});
|
||||
|
||||
@override
|
||||
bool get valid => super.valid && fields.length >= 18;
|
||||
|
||||
/// Módválasztás: 'M' = kézi, 'A' = automatikus
|
||||
String get selectionMode => fields[1];
|
||||
|
||||
/// Fix típus: 1 = nincs, 2 = 2D, 3 = 3D
|
||||
int get fixType => int.tryParse(fields[2]) ?? 1;
|
||||
|
||||
/// Fixben használt műholdak PRN számai (max 12, üres mezők kihagyva)
|
||||
List<int> get activeSatellitePrns {
|
||||
final prns = <int>[];
|
||||
for (int i = 3; i <= 14; i++) {
|
||||
if (i >= fields.length) break;
|
||||
final prn = int.tryParse(fields[i]);
|
||||
if (prn != null && prn > 0) prns.add(prn);
|
||||
}
|
||||
return prns;
|
||||
}
|
||||
|
||||
/// PDOP — 3D pozíció pontossági szorzó
|
||||
double get pdop => _parseDouble(fields[15]);
|
||||
|
||||
/// HDOP — vízszintes pontossági szorzó
|
||||
double get hdop => _parseDouble(fields[16]);
|
||||
|
||||
/// VDOP — függőleges pontossági szorzó
|
||||
/// Az utolsó mező checksum előtti részét veszi
|
||||
double get vdop {
|
||||
if (fields.length <= 17) return 0.0;
|
||||
return _parseDouble(fields[17].split('*').first);
|
||||
}
|
||||
|
||||
/// GDOP számítása: √(PDOP² + TDOP²)
|
||||
/// Közelítés TDOP nélkül: GDOP ≈ PDOP * 1.1
|
||||
double get gdopApprox => pdop * 1.1;
|
||||
|
||||
/// Talker azonosító — rendszer meghatározásához
|
||||
/// GP=GPS, GL=GLONASS, GA=Galileo, GB/BD=BeiDou, GN=vegyes
|
||||
String get talkerId => raw.length >= 3 ? raw.substring(1, 3) : 'GN';
|
||||
|
||||
/// Rendszer neve a talker alapján
|
||||
String get systemName => switch (talkerId) {
|
||||
'GP' => 'GPS',
|
||||
'GL' => 'GLONASS',
|
||||
'GA' => 'Galileo',
|
||||
'GB' || 'BD' => 'BeiDou',
|
||||
'GN' => 'Mixed',
|
||||
_ => 'Unknown',
|
||||
};
|
||||
|
||||
int? get systemId {
|
||||
if (fields.length <= 18) return null;
|
||||
final rawSys = fields[18].split('*').first.trim();
|
||||
if (rawSys.isEmpty) return null;
|
||||
return int.tryParse(rawSys);
|
||||
}
|
||||
|
||||
GnssConstellation get constellation {
|
||||
switch (systemId) {
|
||||
case 1:
|
||||
return GnssConstellation.gps;
|
||||
case 2:
|
||||
return GnssConstellation.glonass;
|
||||
case 3:
|
||||
return GnssConstellation.galileo;
|
||||
case 4:
|
||||
return GnssConstellation.beidou;
|
||||
case 5:
|
||||
return GnssConstellation.qzss;
|
||||
default:
|
||||
return SatelliteInfo.constellationFromTalker(talkerId);
|
||||
}
|
||||
}
|
||||
|
||||
String get constellationKey => constellation.name;
|
||||
|
||||
List<String> get activeSatelliteKeys =>
|
||||
activeSatellitePrns.map((prn) => '$constellationKey:$prn').toList();
|
||||
|
||||
int get usedSatelliteCount => activeSatellitePrns.length;
|
||||
|
||||
double _parseDouble(String? s) {
|
||||
if (s == null || s.isEmpty) return 0.0;
|
||||
return double.tryParse(s) ?? 0.0;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,37 @@
|
||||
import 'package:nmea/nmea.dart';
|
||||
|
||||
class Gngst extends TalkerSentence {
|
||||
static const String id = "GNGST";
|
||||
static const String id = 'GxGST';
|
||||
|
||||
Gngst({required super.raw});
|
||||
|
||||
@override
|
||||
bool get valid => super.valid && fields.length == 9;
|
||||
bool get valid => super.valid && fields.length >= 9;
|
||||
|
||||
String get utcOfPositionFix => fields[1];
|
||||
double get rms => double.parse(fields[2]);
|
||||
double get errorEllipseSemiMajorAxis => double.parse(fields[3]);
|
||||
double get errorEllipseSemiMinorAxis => double.parse(fields[4]);
|
||||
double get errorEllipseOrientation => double.parse(fields[5]);
|
||||
double get longitudeError => double.parse(fields[6]);
|
||||
double get latitudeError => double.parse(fields[7]);
|
||||
double get heightError => double.parse(fields[8]);
|
||||
String get utcOfPositionFix => _field(1);
|
||||
|
||||
double get rms => _parseDouble(_field(2), fallback: 0.0);
|
||||
|
||||
double get errorEllipseSemiMajorAxis =>
|
||||
_parseDouble(_field(3), fallback: 0.0);
|
||||
|
||||
double get errorEllipseSemiMinorAxis =>
|
||||
_parseDouble(_field(4), fallback: 0.0);
|
||||
|
||||
double get errorEllipseOrientation => _parseDouble(_field(5), fallback: 0.0);
|
||||
|
||||
double get longitudeError => _parseDouble(_field(6), fallback: 0.0);
|
||||
|
||||
double get latitudeError => _parseDouble(_field(7), fallback: 0.0);
|
||||
|
||||
double get heightError => _parseDouble(_field(8), fallback: 0.0);
|
||||
|
||||
String _field(int index) {
|
||||
if (index < 0 || index >= fields.length) return '';
|
||||
return fields[index].trim();
|
||||
}
|
||||
|
||||
double _parseDouble(String value, {required double fallback}) {
|
||||
return double.tryParse(value) ?? fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// ─── GSV mondatelemző ──────────────────────────────────────────────────────
|
||||
|
||||
import 'package:nmea/nmea.dart';
|
||||
import 'package:terepi_seged/models/satellite_info.dart';
|
||||
|
||||
/// GSV — GNSS Satellites in View
|
||||
///
|
||||
/// Formátum (max 4 műhold/mondat):
|
||||
/// $GPGSV,3,1,11,01,70,042,45,03,55,218,48,07,22,085,35,10,35,315,40*73
|
||||
/// │ │ │ │ │ │ │
|
||||
/// │ │ │ PRN El Az SNR (×4 max)
|
||||
/// │ │ └── összes látható műhold száma
|
||||
/// │ └───── ezen mondat sorszáma (1-től)
|
||||
/// └─────── összes GSV mondat ebben a ciklusban
|
||||
///
|
||||
/// Több mondat alkotja az epochot:
|
||||
/// GPGSV (GPS) + GLGSV (GLONASS) + GAGSV (Galileo) + GBGSV (BeiDou)
|
||||
class Gngsv extends TalkerSentence {
|
||||
static const String id = 'GNGSV';
|
||||
|
||||
Gngsv({required super.raw});
|
||||
|
||||
@override
|
||||
bool get valid =>
|
||||
super.valid && fields.length >= 8; // min: típus + 3 fejléc + 1×4 műhold
|
||||
|
||||
// ── Fejléc mezők ─────────────────────────────────────────────────
|
||||
|
||||
/// Összes GSV mondat ezen rendszerhez ebben az epochban
|
||||
int get totalMessages => int.tryParse(fields[1]) ?? 1;
|
||||
|
||||
/// Az aktuális mondat sorszáma
|
||||
int get messageNumber => int.tryParse(fields[2]) ?? 1;
|
||||
|
||||
/// Összes látható műhold száma az adott stream-ben
|
||||
int get totalSatellitesInView => int.tryParse(fields[3]) ?? 0;
|
||||
|
||||
/// Ez az utolsó mondat az adott rendszer GSV sorozatában?
|
||||
bool get isLastMessage => messageNumber == totalMessages;
|
||||
|
||||
String get talkerId => raw.length >= 3 ? raw.substring(1, 3) : 'GN';
|
||||
|
||||
GnssConstellation get constellation =>
|
||||
SatelliteInfo.constellationFromTalker(talkerId);
|
||||
|
||||
String get systemName => switch (constellation) {
|
||||
GnssConstellation.gps => 'GPS',
|
||||
GnssConstellation.glonass => 'GLONASS',
|
||||
GnssConstellation.galileo => 'Galileo',
|
||||
GnssConstellation.beidou => 'BeiDou',
|
||||
GnssConstellation.qzss => 'QZSS',
|
||||
GnssConstellation.sbas => 'SBAS',
|
||||
GnssConstellation.navic => 'NavIC',
|
||||
GnssConstellation.mixed => 'Mixed',
|
||||
GnssConstellation.unknown => 'Unknown',
|
||||
};
|
||||
|
||||
/// Opcionális NMEA Signal ID
|
||||
///
|
||||
/// Header: 4 mezo
|
||||
/// Minden muhold: 4 mezo
|
||||
/// Ha marad +1 mezo, az a signalId
|
||||
int? get signalId {
|
||||
final payloadCount = fields.length - 4;
|
||||
if (payloadCount <= 0) return null;
|
||||
|
||||
final hasSignalId = payloadCount % 4 == 1;
|
||||
if (!hasSignalId) return null;
|
||||
|
||||
final rawSignal = fields.last.split('*').first.trim();
|
||||
if (rawSignal.isEmpty) return null;
|
||||
return int.tryParse(rawSignal);
|
||||
}
|
||||
|
||||
/// Egy stream kulcsa: ugyanazon talker, ugyanazon signal
|
||||
String get streamKey => '$talkerId:${signalId ?? -1}';
|
||||
|
||||
// ── Műholdak ebben a mondatban ───────────────────────────────────
|
||||
|
||||
/// A mondatban szereplő műholdak listája (max 4)
|
||||
List<SatelliteInfo> get satellites {
|
||||
final sats = <SatelliteInfo>[];
|
||||
|
||||
// Minden műhold 4 mezőt foglal: PRN, eleváció, azimut, SNR
|
||||
// fields[4]-től kezdődnek, fields[0] = mondattípus
|
||||
for (int i = 0; i < 4; i++) {
|
||||
final base = 4 + i * 4;
|
||||
|
||||
// Ellenőrzés: van-e elég mező
|
||||
if (base + 3 >= fields.length) break;
|
||||
|
||||
final prn = int.tryParse(_field(base));
|
||||
final elev = int.tryParse(_field(base + 1)) ?? 0;
|
||||
final az = int.tryParse(_field(base + 2)) ?? 0;
|
||||
final snr = int.tryParse(_field(base + 3).split('*').first) ?? 0;
|
||||
|
||||
if (prn != null && prn > 0) {
|
||||
sats.add(SatelliteInfo(
|
||||
prn: prn,
|
||||
elevation: elev,
|
||||
azimuth: az,
|
||||
snr: snr,
|
||||
constellation: constellation,
|
||||
signalId: signalId,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return sats;
|
||||
}
|
||||
|
||||
String _field(int index) {
|
||||
if (index < 0 || index >= fields.length) return '';
|
||||
return fields[index].trim();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,71 @@
|
||||
import 'package:nmea/nmea.dart';
|
||||
|
||||
class Gnrmc extends TalkerSentence {
|
||||
static const String id = "GNRMC";
|
||||
static const String id = 'GxRMC';
|
||||
|
||||
Gnrmc({required super.raw});
|
||||
|
||||
@override
|
||||
bool get valid => super.valid && fields.length == 14;
|
||||
bool get valid => super.valid && fields.length >= 10;
|
||||
|
||||
String get messageId => fields[0];
|
||||
String get utcOfPositionFix => fields[1];
|
||||
String get Status => fields[2];
|
||||
double get latitude => _getCoordinateFromGpsLatitude(fields[3]);
|
||||
String get latitudeDirection => fields[4];
|
||||
double get longitude => _getCoordinateFromGpsLongitude(fields[5]);
|
||||
String get longitudeDirection => fields[6];
|
||||
double get speedOverGroundKnots => double.parse(fields[7]);
|
||||
double get courseOverGround => double.parse(fields[8]);
|
||||
String get date => fields[9];
|
||||
double get magneticVariation => double.parse(fields[10]);
|
||||
String get magneticVariationDirection => fields[11];
|
||||
String get positioningSystemModeIndicator => fields[12];
|
||||
String get messageId => _field(0);
|
||||
|
||||
double _getCoordinateFromGpsLongitude(String coordinate) {
|
||||
double result =
|
||||
double.parse("${coordinate[0]}${coordinate[1]}${coordinate[2]}") +
|
||||
double.parse(coordinate.substring(3)) / 60;
|
||||
String get utcOfPositionFix => _field(1);
|
||||
|
||||
return result;
|
||||
String get status => _field(2).toUpperCase();
|
||||
|
||||
String get latitudeDirection => _field(4).toUpperCase();
|
||||
|
||||
double get latitude => _parseLatitude(_field(3), latitudeDirection) ?? 0.0;
|
||||
|
||||
String get longitudeDirection => _field(6).toUpperCase();
|
||||
|
||||
double get longitude => _parseLongitude(_field(5), longitudeDirection) ?? 0.0;
|
||||
|
||||
double get speedOverGroundKnots => _parseDouble(_field(7), fallback: 0.0);
|
||||
|
||||
double get courseOverGround => _parseDouble(_field(8), fallback: 0.0);
|
||||
|
||||
String get date => _field(9);
|
||||
|
||||
// Opcionális mezők -> nullable getterek
|
||||
double? get magneticVariation => _parseDoubleOrNull(_field(10));
|
||||
|
||||
String get magneticVariationDirection => _field(11).toUpperCase();
|
||||
|
||||
String get positioningSystemModeIndicator => _field(12).toUpperCase();
|
||||
|
||||
String _field(int index) {
|
||||
if (index < 0 || index >= fields.length) return '';
|
||||
return fields[index].trim();
|
||||
}
|
||||
|
||||
double _getCoordinateFromGpsLatitude(String coordinate) {
|
||||
double result = double.parse("${coordinate[0]}${coordinate[1]}") +
|
||||
double.parse(coordinate.substring(2)) / 60;
|
||||
double _parseDouble(String value, {required double fallback}) {
|
||||
return double.tryParse(value) ?? fallback;
|
||||
}
|
||||
|
||||
return result;
|
||||
double? _parseDoubleOrNull(String value) {
|
||||
if (value.isEmpty) return null;
|
||||
return double.tryParse(value);
|
||||
}
|
||||
|
||||
double? _parseLatitude(String value, String direction) {
|
||||
if (value.length < 4) return null; // ddmm.mmmm
|
||||
final deg = double.tryParse(value.substring(0, 2));
|
||||
final min = double.tryParse(value.substring(2));
|
||||
if (deg == null || min == null) return null;
|
||||
|
||||
final result = deg + (min / 60.0);
|
||||
return direction == 'S' ? -result : result;
|
||||
}
|
||||
|
||||
double? _parseLongitude(String value, String direction) {
|
||||
if (value.length < 5) return null; // dddmm.mmmm
|
||||
final deg = double.tryParse(value.substring(0, 3));
|
||||
final min = double.tryParse(value.substring(3));
|
||||
if (deg == null || min == null) return null;
|
||||
|
||||
final result = deg + (min / 60.0);
|
||||
return direction == 'W' ? -result : result;
|
||||
}
|
||||
}
|
||||
|
||||
+90
-3
@@ -1,17 +1,104 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||
import 'package:terepi_seged/pages/update_required/presentation/views/update_required_view.dart';
|
||||
import 'package:terepi_seged/routes/app_pages.dart';
|
||||
import 'package:terepi_seged/services/app_database.dart';
|
||||
import 'package:terepi_seged/services/app_logger.dart';
|
||||
import 'package:terepi_seged/services/auth_service.dart';
|
||||
import 'package:terepi_seged/services/contact_service.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
import 'package:terepi_seged/services/device_identity_service.dart';
|
||||
import 'package:terepi_seged/services/firebase_logger.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/layer_import_service.dart';
|
||||
import 'package:terepi_seged/services/layer_sync_service.dart';
|
||||
import 'package:terepi_seged/services/note_audio_service.dart';
|
||||
import 'package:terepi_seged/services/note_photo_service.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
import 'package:terepi_seged/services/permission_service.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_sync_service.dart';
|
||||
import 'package:terepi_seged/services/tilt_service.dart';
|
||||
import 'package:terepi_seged/services/track_sync_service.dart';
|
||||
import 'package:terepi_seged/services/ts_sync_service.dart';
|
||||
import 'package:terepi_seged/services/version_gate_service.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp();
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// A Supabase session-automata frissítése saját háttér-időzítővel fut;
|
||||
// terepen, jel nélkül ez RENDSZERESEN SocketException/
|
||||
// AuthRetryableFetchException-nel száll el — ez NEM app-összeomlás,
|
||||
// a GoTrue a következő ciklusban magától újrapróbálja. A saját,
|
||||
// explicit bejelentkezési hívásainkat már helyben elkapjuk
|
||||
// (AuthService.signInWithGoogle), ezért ide csak ez az ártalmatlan,
|
||||
// háttér-eredetű eset juthat el — nem fatal-ként rögzítjük, hogy ne
|
||||
// torzítsa a crash-free arányt és ne riasszon feleslegesen.
|
||||
if (error is AuthException) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
|
||||
return true;
|
||||
}
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
await dotenv.load(fileName: ".env");
|
||||
|
||||
await Supabase.initialize(
|
||||
url: 'https://supa.app-dev.hu',
|
||||
anonKey:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzExMDYyMDAwLAogICJleHAiOiAxODY4ODI4NDAwCn0.XWtP3eEysZDxXjaHHUZyyhw0n4YZo_xWUMWS5ajBcbI');
|
||||
url: dotenv.env['SUPABASE_URL']!,
|
||||
anonKey: dotenv.env['SUPABASE_ANON_KEY']!);
|
||||
|
||||
final versionGate = await checkVersionGate();
|
||||
if (versionGate.blocked) {
|
||||
runApp(MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: UpdateRequiredView(result: versionGate),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
Get.put(AuthService());
|
||||
Get.put(DeviceIdentityService(), permanent: true);
|
||||
await AppDatabase.instance.database;
|
||||
//await AppDatabase.instance.testOnly();
|
||||
Get.put(ProjectService(), permanent: true);
|
||||
|
||||
Get.put(AppLogger(), permanent: true);
|
||||
Get.put(FirebaseLogger(), permanent: true);
|
||||
await Get.putAsync<CoordConverterService>(
|
||||
() => CoordConverterService().init());
|
||||
Get.put(GnssDeviceService());
|
||||
Get.put(GnssService());
|
||||
Get.put(StakeoutService());
|
||||
Get.put(NtripService());
|
||||
NtripService.to.onRtcmData = (data) => GnssService.to.sendToReceiver(data);
|
||||
Get.put(TrackingController(), permanent: true);
|
||||
Get.put(NotePhotoService(), permanent: true);
|
||||
Get.put(NoteAudioService(), permanent: true);
|
||||
Get.put(LayerImportService(), permanent: true);
|
||||
Get.put(TrackSyncService(), permanent: true);
|
||||
Get.put(LayerSyncService());
|
||||
Get.put(StakeoutSyncService());
|
||||
Get.put(TsSyncService());
|
||||
Get.put(TiltService());
|
||||
Get.put(PermissionService());
|
||||
Get.put(ContactService());
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:terepi_seged/map_layers/map_layer.dart';
|
||||
|
||||
// import 'map_geojson_layer.dart';
|
||||
|
||||
// class DxfLayer extends MapLayer {
|
||||
// // DXF parsing — nincs Flutter csomag, szerver oldali konverzió kell
|
||||
// // Supabase Edge Function: DXF → GeoJSON (ogr2ogr alapú)
|
||||
// @override
|
||||
// Widget buildFlutterMapLayer() => GeoJsonLayer.fromFeatures(_features);
|
||||
// }
|
||||
@@ -0,0 +1,17 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
// import 'map_layer.dart';
|
||||
|
||||
// class GeoJsonLayer extends MapLayer {
|
||||
// final String sourceUrl; // Supabase Storage URL
|
||||
// final String? localPath; // offline cache
|
||||
// FeatureCollection? _data;
|
||||
|
||||
// @override
|
||||
// Widget buildFlutterMapLayer() {
|
||||
// if (_data == null) return const SizedBox.shrink();
|
||||
// return PolygonLayer(polygons: _buildPolygons());
|
||||
// // vagy PolylineLayer / MarkerLayer a geometry type-tól függően
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:terepi_seged/enums/map_layer_type.dart';
|
||||
import 'package:terepi_seged/enums/map_layer_visibility.dart';
|
||||
|
||||
abstract class MapLayer {
|
||||
late final String id;
|
||||
late final String name;
|
||||
late final LayerType type;
|
||||
late LayerVisibility visibility;
|
||||
late double opacity;
|
||||
late int zIndex;
|
||||
//LayerStyle style;
|
||||
|
||||
// Minden réteg tudja magát flutter_map widget-té alakítani
|
||||
Widget buildFlutterMapLayer();
|
||||
|
||||
// Offline cache-elhetőség
|
||||
Future<void> prefetch();
|
||||
bool get isCached;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_map/flutter_map.dart';
|
||||
// import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
// import 'map_layer.dart';
|
||||
|
||||
// class PostGisLayer extends MapLayer {
|
||||
// final String table; // Supabase tábla neve
|
||||
// final String geomColumn; // geometry oszlop
|
||||
// final String? filter; // pl. "project_id=eq.123"
|
||||
// final BoundingBox? bbox; // csak látható terület lekérése
|
||||
|
||||
// @override
|
||||
// Widget buildFlutterMapLayer() {
|
||||
// // flutter_map_tile_caching + PostGIS tile endpoint
|
||||
// return TileLayer(urlTemplate: _buildMvtUrl());
|
||||
// }
|
||||
|
||||
// // MapBox Vector Tiles (MVT) Supabase Edge Function-ből
|
||||
// String _buildMvtUrl() =>
|
||||
// '${Supabase.instance.client.supabaseUrl}/functions/v1/tiles'
|
||||
// '/$table/{z}/{x}/{y}?geom=$geomColumn${filter != null ? "&filter=$filter" : ""}';
|
||||
// }
|
||||
@@ -0,0 +1,11 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// import 'map_geojson_layer.dart';
|
||||
// import 'map_layer.dart';
|
||||
|
||||
// class ShapefileLayer extends MapLayer {
|
||||
// // SHP → GeoJSON konverzió device-on
|
||||
// // shapefile_dart csomag
|
||||
// @override
|
||||
// Widget buildFlutterMapLayer() => GeoJsonLayer.fromFeatures(_features);
|
||||
// }
|
||||
@@ -0,0 +1,76 @@
|
||||
/// Kapcsolat (névjegy) — a Supabase `contacts` táblát tükrözi.
|
||||
///
|
||||
/// Ez a menü CSAK online, bejelentkezett és jogosult felhasználóknak
|
||||
/// érhető el, ezért nincs lokális tükrözés/offline-sync: közvetlenül a
|
||||
/// Supabase-ből olvassuk/írjuk, a hozzáférést az RLS + a jogosultság-
|
||||
/// tábla dönti el. Az uuid szerveroldali (gen_random_uuid()).
|
||||
class Contact {
|
||||
final String? id; // uuid — új rekordnál null, a szerver adja
|
||||
final String
|
||||
projectId; // az AKTUÁLIS projekt uuid-ja (terepi_seged_projects.id)
|
||||
final String name;
|
||||
final String address;
|
||||
final String phone;
|
||||
final String email;
|
||||
final String note;
|
||||
final String? createdBy;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const Contact({
|
||||
this.id,
|
||||
required this.projectId,
|
||||
required this.name,
|
||||
this.address = '',
|
||||
this.phone = '',
|
||||
this.email = '',
|
||||
this.note = '',
|
||||
this.createdBy,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Contact copyWith({
|
||||
String? name,
|
||||
String? address,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? note,
|
||||
}) =>
|
||||
Contact(
|
||||
id: id,
|
||||
projectId: projectId,
|
||||
name: name ?? this.name,
|
||||
address: address ?? this.address,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
note: note ?? this.note,
|
||||
createdBy: createdBy,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
|
||||
/// Beszúráshoz/frissítéshez — az id-t csak akkor küldjük, ha van
|
||||
/// (frissítésnél), a szerveroldali mezőket (created_by, updated_at)
|
||||
/// sosem a kliens állítja.
|
||||
Map<String, dynamic> toWriteMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'project_id': projectId,
|
||||
'name': name.trim(),
|
||||
'address': address.trim(),
|
||||
'phone': phone.trim(),
|
||||
'email': email.trim(),
|
||||
'note': note.trim(),
|
||||
};
|
||||
|
||||
factory Contact.fromMap(Map<String, dynamic> m) => Contact(
|
||||
id: m['id'] as String?,
|
||||
projectId: m['project_id'] as String,
|
||||
name: (m['name'] as String?) ?? '',
|
||||
address: (m['address'] as String?) ?? '',
|
||||
phone: (m['phone'] as String?) ?? '',
|
||||
email: (m['email'] as String?) ?? '',
|
||||
note: (m['note'] as String?) ?? '',
|
||||
createdBy: m['created_by'] as String?,
|
||||
updatedAt: m['updated_at'] != null
|
||||
? DateTime.tryParse(m['updated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Immutable adatosztály — az eszköz összes statikus adata.
|
||||
// A DeviceIdentityService tölti be egyszer induláskor.
|
||||
|
||||
class DeviceInfoModel {
|
||||
// ── Azonosítók ────────────────────────────────────────────────────
|
||||
|
||||
/// Android rendszerszintű egyedi azonosító.
|
||||
/// Reinstall után megmarad, factory reset után változik.
|
||||
final String deviceId;
|
||||
|
||||
/// App-példány UUID — FlutterSecureStorage-ban tárolva.
|
||||
/// Reinstall után új értéket kap.
|
||||
final String appInstanceId;
|
||||
|
||||
// Android vagy IOS
|
||||
final String platform;
|
||||
|
||||
// ── Hardver ───────────────────────────────────────────────────────
|
||||
|
||||
final String manufacturer; // "Samsung"
|
||||
final String model; // "Galaxy Tab S9 Ultra"
|
||||
final String brand; // "samsung"
|
||||
|
||||
// ── Rendszer ──────────────────────────────────────────────────────
|
||||
|
||||
/// Felhasználó által adott eszköznév (pl. "Pista telefonja").
|
||||
/// A Settings.Global.DEVICE_NAME értéke — ez az alapértelmezett label.
|
||||
final String systemDeviceName;
|
||||
|
||||
final String osVersion; // "14"
|
||||
final int sdkInt; // 34
|
||||
final String securityPatch; // "2024-06-01"
|
||||
|
||||
// ── App ───────────────────────────────────────────────────────────
|
||||
|
||||
final String appVersion; // "1.0.0"
|
||||
final String buildNumber; // "15"
|
||||
|
||||
const DeviceInfoModel({
|
||||
required this.deviceId,
|
||||
required this.appInstanceId,
|
||||
required this.platform,
|
||||
required this.manufacturer,
|
||||
required this.model,
|
||||
required this.brand,
|
||||
required this.systemDeviceName,
|
||||
required this.osVersion,
|
||||
required this.sdkInt,
|
||||
required this.securityPatch,
|
||||
required this.appVersion,
|
||||
required this.buildNumber,
|
||||
});
|
||||
|
||||
// ── Supabase regisztrációs map ────────────────────────────────────
|
||||
|
||||
Map<String, dynamic> toRegistrationMap({required String label}) => {
|
||||
'device_id': deviceId,
|
||||
'app_instance_id': appInstanceId,
|
||||
'platform': platform,
|
||||
'label': label,
|
||||
'manufacturer': manufacturer,
|
||||
'model': model,
|
||||
'os_version': osVersion,
|
||||
'sdk_int': sdkInt,
|
||||
'security_patch': securityPatch,
|
||||
'app_version': '$appVersion+$buildNumber',
|
||||
'last_seen': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'DeviceInfo($manufacturer $model, Android $osVersion, app $appVersion)';
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Importált réteg modellje.
|
||||
// A GeoJsonParser közvetlenül flutter_map objektumokat gyárt,
|
||||
// ezeket csomagoljuk egy kezelhető rétegbe.
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ImportedLayer {
|
||||
final String id;
|
||||
final String name;
|
||||
final LayerImportSourceType sourceType;
|
||||
|
||||
// flutter_map objektumok — közvetlenül a réteg widgetekbe kerülnek
|
||||
final List<Marker> markers;
|
||||
final List<Polyline> polylines;
|
||||
final List<Polygon> polygons;
|
||||
|
||||
final bool isVisible;
|
||||
final DateTime importedAt;
|
||||
|
||||
const ImportedLayer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceType,
|
||||
required this.markers,
|
||||
required this.polylines,
|
||||
required this.polygons,
|
||||
this.isVisible = true,
|
||||
required this.importedAt,
|
||||
});
|
||||
|
||||
// ── Statisztikák ──────────────────────────────────────────────────
|
||||
|
||||
int get featureCount => markers.length + polylines.length + polygons.length;
|
||||
|
||||
bool get isEmpty => featureCount == 0;
|
||||
|
||||
// ── Bounding box — zoom a rétegre ─────────────────────────────────
|
||||
|
||||
LatLngBounds? get bounds {
|
||||
final points = <LatLng>[
|
||||
...markers.map((m) => m.point),
|
||||
...polylines.expand((p) => p.points),
|
||||
...polygons.expand((p) => p.points),
|
||||
];
|
||||
if (points.isEmpty) return null;
|
||||
|
||||
var minLat = points.first.latitude;
|
||||
var maxLat = points.first.latitude;
|
||||
var minLon = points.first.longitude;
|
||||
var maxLon = points.first.longitude;
|
||||
|
||||
for (final p in points) {
|
||||
if (p.latitude < minLat) minLat = p.latitude;
|
||||
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||
if (p.longitude < minLon) minLon = p.longitude;
|
||||
if (p.longitude > maxLon) maxLon = p.longitude;
|
||||
}
|
||||
|
||||
return LatLngBounds(
|
||||
LatLng(minLat, minLon),
|
||||
LatLng(maxLat, maxLon),
|
||||
);
|
||||
}
|
||||
|
||||
ImportedLayer copyWith({bool? isVisible}) => ImportedLayer(
|
||||
id: id,
|
||||
name: name,
|
||||
sourceType: sourceType,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
polygons: polygons,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
importedAt: importedAt,
|
||||
);
|
||||
|
||||
// ── Gyártó: GeoJsonParser kimenetéből ─────────────────────────────
|
||||
|
||||
static ImportedLayer fromGeoJsonParser({
|
||||
required dynamic parser, // GeoJsonParser
|
||||
required String name,
|
||||
LayerImportSourceType source = LayerImportSourceType.geoJson,
|
||||
}) {
|
||||
return ImportedLayer(
|
||||
id: const Uuid().v4(),
|
||||
name: name,
|
||||
sourceType: source,
|
||||
markers: List.from(parser.markers as List),
|
||||
polylines: List.from(parser.polylines as List),
|
||||
polygons: List.from(parser.polygons as List),
|
||||
importedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||
import 'package:terepi_seged/services/layer_import_service.dart';
|
||||
|
||||
class ImportedLayerMeta {
|
||||
final String id;
|
||||
final String name;
|
||||
final LayerImportSourceType sourceType;
|
||||
final String localPath; // fájl elérési út
|
||||
final String? storagePath; // Supabase Storage (megosztáskor)
|
||||
final bool isVisible;
|
||||
final int? projectId;
|
||||
final DateTime importedAt;
|
||||
final DateTime? syncedAt;
|
||||
final String? colorHex;
|
||||
final double? opacity;
|
||||
final double? strokeWidth;
|
||||
|
||||
const ImportedLayerMeta(
|
||||
{required this.id,
|
||||
required this.name,
|
||||
required this.sourceType,
|
||||
required this.localPath,
|
||||
this.storagePath,
|
||||
this.isVisible = true,
|
||||
this.projectId,
|
||||
required this.importedAt,
|
||||
this.syncedAt,
|
||||
this.colorHex,
|
||||
this.opacity,
|
||||
this.strokeWidth});
|
||||
|
||||
bool get isSynced => storagePath != null;
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_type': sourceType.name,
|
||||
'local_path': localPath,
|
||||
'storage_path': storagePath,
|
||||
'is_visible': isVisible ? 1 : 0,
|
||||
'project_id': projectId,
|
||||
'imported_at': importedAt.toIso8601String(),
|
||||
'synced_at': syncedAt?.toIso8601String(),
|
||||
if (colorHex != null) 'color_hex': colorHex,
|
||||
if (opacity != null) 'opacity': opacity,
|
||||
if (strokeWidth != null) 'stroke_width': strokeWidth
|
||||
};
|
||||
|
||||
factory ImportedLayerMeta.fromMap(Map<String, dynamic> m) =>
|
||||
ImportedLayerMeta(
|
||||
id: m['id'] as String,
|
||||
name: m['name'] as String,
|
||||
sourceType:
|
||||
LayerImportSourceType.values.byName(m['source_type'] as String),
|
||||
localPath: m['local_path'] as String,
|
||||
storagePath: m['storage_path'] as String?,
|
||||
isVisible: (m['is_visible'] as int) == 1,
|
||||
projectId: m['project_id'] as int?,
|
||||
importedAt: DateTime.parse(m['imported_at'] as String),
|
||||
syncedAt: m['synced_at'] != null
|
||||
? DateTime.parse(m['synced_at'] as String)
|
||||
: null,
|
||||
colorHex: m['color_hex'] as String?,
|
||||
opacity: (m['opacity'] as num?)?.toDouble(),
|
||||
strokeWidth: (m['stroke_width'] as num?)?.toDouble());
|
||||
|
||||
ImportedLayerMeta copyWith({
|
||||
bool? isVisible,
|
||||
String? storagePath,
|
||||
DateTime? syncedAt,
|
||||
String? colorHex,
|
||||
double? opacity,
|
||||
double? strokeWidth,
|
||||
}) =>
|
||||
ImportedLayerMeta(
|
||||
id: id,
|
||||
name: name,
|
||||
sourceType: sourceType,
|
||||
localPath: localPath,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
projectId: projectId,
|
||||
importedAt: importedAt,
|
||||
syncedAt: syncedAt ?? this.syncedAt,
|
||||
colorHex: colorHex ?? this.colorHex,
|
||||
opacity: opacity ?? this.opacity,
|
||||
strokeWidth: strokeWidth ?? this.strokeWidth,
|
||||
);
|
||||
|
||||
ImportedLayerStyle? getStyle() {
|
||||
if (colorHex == null) return null;
|
||||
final hex = colorHex!.replaceAll('#', '');
|
||||
final color = Color(int.parse('FF$hex', radix: 16));
|
||||
|
||||
return ImportedLayerStyle(
|
||||
color: color,
|
||||
opacity: opacity ?? 0.35,
|
||||
strokeWidth: strokeWidth ?? 2.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
class MeasuredPoint {
|
||||
final int? id;
|
||||
final int projectId;
|
||||
final String name;
|
||||
final double? eovY;
|
||||
final double? eovX;
|
||||
final double? eovZ;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final double? altitude;
|
||||
final double? accuracy;
|
||||
final int? fixQuality;
|
||||
final DateTime timestamp;
|
||||
final String note;
|
||||
|
||||
const MeasuredPoint({
|
||||
this.id,
|
||||
required this.projectId,
|
||||
required this.name,
|
||||
this.eovY,
|
||||
this.eovX,
|
||||
this.eovZ,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.altitude,
|
||||
this.accuracy,
|
||||
this.fixQuality,
|
||||
required this.timestamp,
|
||||
this.note = '',
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'project_id': projectId,
|
||||
'name': name,
|
||||
'eov_y': eovY,
|
||||
'eov_x': eovX,
|
||||
'eov_z': eovZ,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'altitude': altitude,
|
||||
'accuracy': accuracy,
|
||||
'fix_quality': fixQuality,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'note': note,
|
||||
};
|
||||
|
||||
factory MeasuredPoint.fromMap(Map<String, dynamic> m) => MeasuredPoint(
|
||||
id: m['id'] as int?,
|
||||
projectId: m['project_id'] as int,
|
||||
name: m['name'] as String,
|
||||
eovY: (m['eov_y'] as num?)?.toDouble(),
|
||||
eovX: (m['eov_x'] as num?)?.toDouble(),
|
||||
eovZ: (m['eov_z'] as num?)?.toDouble(),
|
||||
latitude: (m['latitude'] as num?)?.toDouble(),
|
||||
longitude: (m['longitude'] as num?)?.toDouble(),
|
||||
altitude: (m['altitude'] as num?)?.toDouble(),
|
||||
accuracy: (m['accuracy'] as num?)?.toDouble(),
|
||||
fixQuality: m['fix_quality'] as int?,
|
||||
timestamp: DateTime.parse(m['timestamp'] as String),
|
||||
note: m['note'] as String? ?? '',
|
||||
);
|
||||
|
||||
// ── CSV sor ───────────────────────────────────────────────────────
|
||||
|
||||
static String csvHeader() => 'Név;EOV Y;EOV X;EOV Z (tszf);'
|
||||
'Lat (WGS84);Lon (WGS84);Magasság (m);'
|
||||
'Pontosság (m);Fix minőség;Megjegyzés;Időbélyeg';
|
||||
|
||||
String toCsvRow() {
|
||||
String fmt(double? v, int dec) => v == null ? '' : v.toStringAsFixed(dec);
|
||||
return '${_csv(name)};'
|
||||
'${fmt(eovY, 1)};${fmt(eovX, 1)};${fmt(eovZ, 3)};'
|
||||
'${fmt(latitude, 8)};${fmt(longitude, 8)};${fmt(altitude, 3)};'
|
||||
'${fmt(accuracy, 4)};${fixQuality ?? ""};'
|
||||
'${_csv(note)};${timestamp.toIso8601String()}';
|
||||
}
|
||||
|
||||
/// Pontosvesszőt tartalmazó mezőket idézőjelbe teszi
|
||||
String _csv(String v) => v.contains(';') ? '"$v"' : v;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../enums/note_type.dart';
|
||||
|
||||
class NoteItem {
|
||||
final int? id;
|
||||
final int? projectId;
|
||||
final NoteType type;
|
||||
final List<LatLng> points; // ← szerkesztéshez mindig elérhető
|
||||
final Color color;
|
||||
final double opacity;
|
||||
final double strokeWidth;
|
||||
final Color strokeColor;
|
||||
final String label;
|
||||
final DateTime createdAt;
|
||||
|
||||
const NoteItem(
|
||||
{this.id,
|
||||
this.projectId,
|
||||
required this.type,
|
||||
required this.points,
|
||||
required this.color,
|
||||
this.opacity = 0.5,
|
||||
this.strokeWidth = 3.0,
|
||||
required this.strokeColor,
|
||||
this.label = '',
|
||||
required this.createdAt});
|
||||
|
||||
NoteItem copyWith({
|
||||
List<LatLng>? points,
|
||||
Color? color,
|
||||
double? opacity,
|
||||
double? strokeWidth,
|
||||
Color? strokeColor,
|
||||
String? label,
|
||||
}) =>
|
||||
NoteItem(
|
||||
id: id,
|
||||
projectId: projectId,
|
||||
type: type,
|
||||
points: points ?? this.points,
|
||||
color: color ?? this.color,
|
||||
opacity: opacity ?? this.opacity,
|
||||
strokeWidth: strokeWidth ?? this.strokeWidth,
|
||||
strokeColor: strokeColor ?? this.strokeColor,
|
||||
label: label ?? this.label,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
// ── Koordináta ↔ GeoJSON ────────────────────────────────────────
|
||||
|
||||
static List<LatLng> _parsePoints(String json) {
|
||||
final geom = jsonDecode(json) as Map<String, dynamic>;
|
||||
final gType = geom['type'] as String;
|
||||
final coords = geom['coordinates'];
|
||||
|
||||
switch (gType) {
|
||||
case 'Point':
|
||||
final c = coords as List;
|
||||
return [
|
||||
LatLng(
|
||||
(c[1] as num).toDouble(),
|
||||
(c[0] as num).toDouble(),
|
||||
)
|
||||
];
|
||||
|
||||
case 'LineString':
|
||||
return (coords as List)
|
||||
.map((c) => LatLng(
|
||||
(c[1] as num).toDouble(),
|
||||
(c[0] as num).toDouble(),
|
||||
))
|
||||
.toList();
|
||||
|
||||
case 'Polygon':
|
||||
// Külső gyűrű (index 0)
|
||||
return ((coords as List)[0] as List)
|
||||
.map((c) => LatLng(
|
||||
(c[1] as num).toDouble(),
|
||||
(c[0] as num).toDouble(),
|
||||
))
|
||||
.toList();
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
String _toPointsJson() {
|
||||
List<dynamic> coords;
|
||||
String gType;
|
||||
|
||||
switch (type) {
|
||||
case NoteType.point:
|
||||
// GeoJSON: [lon, lat]
|
||||
gType = 'Point';
|
||||
coords = [points.first.longitude, points.first.latitude];
|
||||
|
||||
case NoteType.line:
|
||||
gType = 'LineString';
|
||||
coords = points.map((p) => [p.longitude, p.latitude]).toList();
|
||||
|
||||
case NoteType.polygon:
|
||||
gType = 'Polygon';
|
||||
final ring = points.map((p) => [p.longitude, p.latitude]).toList();
|
||||
// Polygon zárt: első = utolsó pont
|
||||
if (ring.isNotEmpty) {
|
||||
final first = ring.first;
|
||||
final last = ring.last;
|
||||
final isClosed = first[0] == last[0] && first[1] == last[1];
|
||||
|
||||
if (!isClosed) ring.add(List.from(first));
|
||||
}
|
||||
coords = [ring];
|
||||
}
|
||||
|
||||
return jsonEncode({'type': gType, 'coordinates': coords});
|
||||
}
|
||||
// ── SQLite ↔ Map ────────────────────────────────────────────────
|
||||
|
||||
static Color _parseColor(String hex) {
|
||||
final h = hex.replaceFirst('#', '');
|
||||
return Color(int.parse(h.length == 6 ? 'FF$h' : h, radix: 16));
|
||||
}
|
||||
|
||||
static String _colorHex(Color c) =>
|
||||
'#${c.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}';
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
if (projectId != null) 'project_id': projectId,
|
||||
'type': type.name,
|
||||
'points_json': _toPointsJson(),
|
||||
'color': _colorHex(color),
|
||||
'opacity': opacity,
|
||||
'stroke_width': strokeWidth,
|
||||
'stroke_color': _colorHex(strokeColor),
|
||||
'label': label,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
factory NoteItem.fromMap(Map<String, dynamic> m) => NoteItem(
|
||||
id: m['id'] as int?,
|
||||
projectId: m['project_id'] as int?,
|
||||
type: NoteType.values.firstWhere((t) => t.name == (m['type'] as String),
|
||||
orElse: () => NoteType.point),
|
||||
points: _parsePoints(m['points_json'] as String),
|
||||
color: _parseColor(m['color'] as String? ?? '#185FA5'),
|
||||
opacity: (m['opacity'] as num?)?.toDouble() ?? 0.5,
|
||||
strokeWidth: (m['stroke_width'] as num?)?.toDouble() ?? 3.0,
|
||||
strokeColor: _parseColor(m['stroke_color'] as String? ?? '#FFD700'),
|
||||
label: m['label'] as String? ?? '',
|
||||
createdAt: DateTime.parse(m['created_at'] as String),
|
||||
);
|
||||
|
||||
/// Kitöltési szín az opacity-val alkalmazva.
|
||||
Color get fillColor => Color.fromARGB(
|
||||
(opacity * 255).round(),
|
||||
color.red,
|
||||
color.green,
|
||||
color.blue,
|
||||
);
|
||||
|
||||
// Rendereléshez — a hitValue hordozza az id-t a tap detektáláshoz
|
||||
Polyline<int> toPolyline() => Polyline(
|
||||
points: points,
|
||||
color: color,
|
||||
strokeWidth: strokeWidth,
|
||||
hitValue: id!,
|
||||
);
|
||||
|
||||
Polygon<int> toPolygon() => Polygon(
|
||||
points: points,
|
||||
color: color.withOpacity(opacity),
|
||||
borderColor: strokeColor,
|
||||
borderStrokeWidth: strokeWidth,
|
||||
label: label.isEmpty ? null : label,
|
||||
hitValue: id!,
|
||||
labelStyle: TextStyle(fontSize: 10.0));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'dart:io';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class NoteItemAudio {
|
||||
final int? id;
|
||||
final int noteItemId;
|
||||
final String localPath; // .m4a fájl
|
||||
final String caption; // opcionális felirat
|
||||
final int durationSeconds; // másodpercben
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final DateTime createdAt;
|
||||
|
||||
const NoteItemAudio({
|
||||
this.id,
|
||||
required this.noteItemId,
|
||||
required this.localPath,
|
||||
this.caption = '',
|
||||
this.durationSeconds = 0,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
File get file => File(localPath);
|
||||
bool get fileExists => file.existsSync();
|
||||
LatLng? get location => latitude != null && longitude != null
|
||||
? LatLng(latitude!, longitude!)
|
||||
: null;
|
||||
|
||||
/// Formázott időtartam: "0:42" vagy "1:23"
|
||||
String get durationFormatted {
|
||||
final m = durationSeconds ~/ 60;
|
||||
final s = (durationSeconds % 60).toString().padLeft(2, '0');
|
||||
return '$m:$s';
|
||||
}
|
||||
|
||||
NoteItemAudio copyWith({String? caption}) => NoteItemAudio(
|
||||
id: id,
|
||||
noteItemId: noteItemId,
|
||||
localPath: localPath,
|
||||
caption: caption ?? this.caption,
|
||||
durationSeconds: durationSeconds,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'note_item_id': noteItemId,
|
||||
'local_path': localPath,
|
||||
'caption': caption,
|
||||
'duration_seconds': durationSeconds,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
factory NoteItemAudio.fromMap(Map<String, dynamic> m) => NoteItemAudio(
|
||||
id: m['id'] as int?,
|
||||
noteItemId: m['note_item_id'] as int,
|
||||
localPath: m['local_path'] as String,
|
||||
caption: m['caption'] as String? ?? '',
|
||||
durationSeconds: m['duration_seconds'] as int? ?? 0,
|
||||
latitude: (m['latitude'] as num?)?.toDouble(),
|
||||
longitude: (m['longitude'] as num?)?.toDouble(),
|
||||
createdAt: DateTime.parse(m['created_at'] as String),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// lib/models/note_item_photo.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class NoteItemPhoto {
|
||||
final int? id;
|
||||
final int noteItemId;
|
||||
final String localPath; // teljes elérési út a fájlhoz
|
||||
final String? storagePath; // Supabase Storage (megosztáskor)
|
||||
final String caption; // opcionális megjegyzés
|
||||
final double? latitude; // ahol a fotó készült
|
||||
final double? longitude;
|
||||
final DateTime createdAt;
|
||||
|
||||
const NoteItemPhoto({
|
||||
this.id,
|
||||
required this.noteItemId,
|
||||
required this.localPath,
|
||||
this.storagePath,
|
||||
this.caption = '',
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
File get file => File(localPath);
|
||||
bool get fileExists => file.existsSync();
|
||||
bool get isShared => storagePath != null;
|
||||
LatLng? get location => latitude != null && longitude != null
|
||||
? LatLng(latitude!, longitude!)
|
||||
: null;
|
||||
|
||||
NoteItemPhoto copyWith({
|
||||
String? caption,
|
||||
String? storagePath,
|
||||
}) =>
|
||||
NoteItemPhoto(
|
||||
id: id,
|
||||
noteItemId: noteItemId,
|
||||
localPath: localPath,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
caption: caption ?? this.caption,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'note_item_id': noteItemId,
|
||||
'local_path': localPath,
|
||||
'storage_path': storagePath,
|
||||
'caption': caption,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
factory NoteItemPhoto.fromMap(Map<String, dynamic> m) => NoteItemPhoto(
|
||||
id: m['id'] as int?,
|
||||
noteItemId: m['note_item_id'] as int,
|
||||
localPath: m['local_path'] as String,
|
||||
storagePath: m['storage_path'] as String?,
|
||||
caption: m['caption'] as String? ?? '',
|
||||
latitude: (m['latitude'] as num?)?.toDouble(),
|
||||
longitude: (m['longitude'] as num?)?.toDouble(),
|
||||
createdAt: DateTime.parse(m['created_at'] as String),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
class PointWithDescription {
|
||||
final int Id;
|
||||
final int pointId;
|
||||
final DateTime dateTime;
|
||||
final String description;
|
||||
final double eovX;
|
||||
@@ -10,7 +10,7 @@ class PointWithDescription {
|
||||
final double verticalError;
|
||||
|
||||
PointWithDescription(
|
||||
this.Id,
|
||||
this.pointId,
|
||||
this.dateTime,
|
||||
this.description,
|
||||
this.eovX,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
enum ProjectStatus { active, archived }
|
||||
|
||||
enum ProjectCrs { eov, wgs84 }
|
||||
|
||||
class Project {
|
||||
final int? id;
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String client; // megrendelő
|
||||
final String description;
|
||||
final ProjectCrs crs;
|
||||
final String color; // hex, a UI-ban azonosításhoz
|
||||
final ProjectStatus status;
|
||||
final bool isDefault;
|
||||
final bool isLocalOnly;
|
||||
final DateTime? lastSyncedAt;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Project({
|
||||
this.id,
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
this.client = '',
|
||||
this.description = '',
|
||||
this.crs = ProjectCrs.eov,
|
||||
this.color = '#185FA5',
|
||||
this.status = ProjectStatus.active,
|
||||
this.isDefault = false,
|
||||
this.isLocalOnly = false,
|
||||
this.lastSyncedAt,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
Project copyWith(
|
||||
{String? name,
|
||||
String? client,
|
||||
String? description,
|
||||
ProjectCrs? crs,
|
||||
String? color,
|
||||
ProjectStatus? status}) =>
|
||||
Project(
|
||||
id: id,
|
||||
uuid: uuid,
|
||||
name: name ?? this.name,
|
||||
client: client ?? this.client,
|
||||
description: description ?? this.description,
|
||||
crs: crs ?? this.crs,
|
||||
color: color ?? this.color,
|
||||
status: status ?? this.status,
|
||||
isDefault: isDefault,
|
||||
isLocalOnly: isLocalOnly,
|
||||
lastSyncedAt: lastSyncedAt,
|
||||
createdAt: createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'client': client,
|
||||
'description': description,
|
||||
'crs': crs.name,
|
||||
'color': color,
|
||||
'status': status.name,
|
||||
'is_default': isDefault ? 1 : 0,
|
||||
'is_local_only': isLocalOnly ? 1 : 0,
|
||||
if (lastSyncedAt != null)
|
||||
'last_synced_at': lastSyncedAt!.toIso8601String(),
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
factory Project.fromMap(Map<String, dynamic> m) => Project(
|
||||
id: m['id'] as int?,
|
||||
uuid: m['uuid'] as String,
|
||||
name: m['name'] as String,
|
||||
client: m['client'] as String? ?? '',
|
||||
description: m['description'] as String? ?? '',
|
||||
crs: ProjectCrs.values.byName(m['crs'] as String? ?? 'eov'),
|
||||
color: m['color'] as String? ?? '#185FA5',
|
||||
status: ProjectStatus.values.byName(m['status'] as String? ?? 'active'),
|
||||
isDefault: _readBool(m, 'is_default', defaultValue: false),
|
||||
isLocalOnly: _readBool(m, 'is_local_only', defaultValue: false),
|
||||
lastSyncedAt: m['last_synced_at'] == null
|
||||
? null
|
||||
: DateTime.parse(m['last_synced_at'] as String),
|
||||
createdAt: DateTime.parse(m['created_at'] as String),
|
||||
updatedAt: DateTime.parse(m['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
bool _readBool(
|
||||
Map<String, dynamic> m,
|
||||
String key, {
|
||||
bool defaultValue = false,
|
||||
}) {
|
||||
final value = m[key];
|
||||
|
||||
if (value == null) return defaultValue;
|
||||
if (value is bool) return value;
|
||||
if (value is int) return value != 0;
|
||||
|
||||
if (value is String) {
|
||||
final lower = value.toLowerCase();
|
||||
return lower == 'true' || lower == '1';
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
enum GnssConstellation {
|
||||
gps,
|
||||
glonass,
|
||||
galileo,
|
||||
beidou,
|
||||
qzss,
|
||||
sbas,
|
||||
navic,
|
||||
mixed,
|
||||
unknown,
|
||||
}
|
||||
|
||||
// ─── SatelliteInfo modell ──────────────────────────────────────────────────
|
||||
|
||||
/// Egyetlen látható műhold adatai — a skyplot és az SNR diagram alapja.
|
||||
class SatelliteInfo {
|
||||
/// Műhold PRN/SVID azonosítója
|
||||
final int prn;
|
||||
|
||||
/// Eleváció fokban (0–90°, 90 = zenit)
|
||||
final int elevation;
|
||||
|
||||
/// Azimut fokban (0–359°, 0/360 = É, 90 = K)
|
||||
final int azimuth;
|
||||
|
||||
/// Jelerősség dB-Hz-ben (0–99, 0 = nem tracking)
|
||||
final int snr;
|
||||
|
||||
/// A műhold konstellációja
|
||||
final GnssConstellation constellation;
|
||||
|
||||
// Opcionális: NMEA signal ID (pl. L1/L2/E1/...)
|
||||
final int? signalId;
|
||||
|
||||
const SatelliteInfo(
|
||||
{required this.prn,
|
||||
required this.elevation,
|
||||
required this.azimuth,
|
||||
required this.snr,
|
||||
required this.constellation,
|
||||
this.signalId});
|
||||
|
||||
String get system => switch (constellation) {
|
||||
GnssConstellation.gps => 'GPS',
|
||||
GnssConstellation.glonass => 'GLONASS',
|
||||
GnssConstellation.galileo => 'Galileo',
|
||||
GnssConstellation.beidou => 'BeiDou',
|
||||
GnssConstellation.qzss => 'QZSS',
|
||||
GnssConstellation.sbas => 'SBAS',
|
||||
GnssConstellation.navic => 'NavIC',
|
||||
GnssConstellation.mixed => 'Mixed',
|
||||
GnssConstellation.unknown => 'Unknown',
|
||||
};
|
||||
|
||||
/// Egyedi kulcs ugyanazon konstellacio azonos muholdjahoz
|
||||
String get satelliteKey => '${constellation.name}:$prn';
|
||||
|
||||
/// Egyedi kulcs ugyanazon signal streamhez
|
||||
String get signalKey => '${constellation.name}:$prn:${signalId ?? -1}';
|
||||
|
||||
/// Human-readable sav/frekvencia becsles signalId alapjan
|
||||
String get bandLabel => _bandLabel(constellation, signalId);
|
||||
|
||||
// ── Minősítési segédek ────────────────────────────────────────────
|
||||
|
||||
/// Erős jel — fixben megbízhatóan részt vesz
|
||||
bool get isStrong => snr >= 40;
|
||||
|
||||
/// Jelszint alapjan varhatoan hasznalhato jel.
|
||||
/// A valodi "used in fix" allapotot a GSA mondatok alapjan allapitsuk meg.
|
||||
bool get hasUsableSignal => snr >= 30;
|
||||
|
||||
/// Gyenge de látható jel
|
||||
bool get isWeak => snr > 0 && snr < 30;
|
||||
|
||||
/// Egyáltalán látható-e (snr > 0)
|
||||
bool get isVisible => snr > 0;
|
||||
|
||||
// ── Polár koordináta számítás a skyplot CustomPainter-hez ────────
|
||||
|
||||
/// Normalizált sugár (0.0 = zenit, 1.0 = horizont)
|
||||
double get polarRadius => 1.0 - (elevation / 90.0);
|
||||
|
||||
/// Szög radiánban a polár koordinátához
|
||||
/// (azimut → matematikai szög: É=felső, K=jobb)
|
||||
double get polarAngleRad => (azimuth - 90) * math.pi / 180.0;
|
||||
|
||||
/// Polár koordináta pixel pozíciójának kiszámítása
|
||||
/// [center] a kör középpontja, [radius] a horizont kör sugara
|
||||
({double dx, double dy}) toOffset(
|
||||
double centerX, double centerY, double radius) {
|
||||
return (
|
||||
dx: centerX + radius * polarRadius * math.cos(polarAngleRad),
|
||||
dy: centerY + radius * polarRadius * math.sin(polarAngleRad),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final signal = signalId == null ? '-' : signalId.toString();
|
||||
return 'SatelliteInfo($system PRN$prn elev=$elevation az=$azimuth snr=$snr signal=$signal band=$bandLabel)';
|
||||
}
|
||||
|
||||
static GnssConstellation constellationFromTalker(String talkerId) {
|
||||
switch (talkerId) {
|
||||
case 'GP':
|
||||
return GnssConstellation.gps;
|
||||
case 'GL':
|
||||
return GnssConstellation.glonass;
|
||||
case 'GA':
|
||||
return GnssConstellation.galileo;
|
||||
case 'GB':
|
||||
case 'BD':
|
||||
return GnssConstellation.beidou;
|
||||
case 'GQ':
|
||||
case 'QZ':
|
||||
return GnssConstellation.qzss;
|
||||
case 'GI':
|
||||
case 'IN':
|
||||
return GnssConstellation.navic;
|
||||
case 'GN':
|
||||
return GnssConstellation.mixed;
|
||||
default:
|
||||
return GnssConstellation.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _bandLabel(GnssConstellation constellation, int? signalId) {
|
||||
if (signalId == null) return 'Unknown';
|
||||
|
||||
switch (constellation) {
|
||||
case GnssConstellation.gps:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'L1 C/A';
|
||||
case 5:
|
||||
return 'L2C';
|
||||
case 6:
|
||||
return 'L5';
|
||||
default:
|
||||
return 'GPS sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.glonass:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'G1';
|
||||
case 3:
|
||||
return 'G2';
|
||||
default:
|
||||
return 'GLO sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.galileo:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'E1';
|
||||
case 6:
|
||||
return 'E5a';
|
||||
case 7:
|
||||
return 'E5b';
|
||||
case 8:
|
||||
return 'E5 AltBOC';
|
||||
default:
|
||||
return 'GAL sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.beidou:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'B1I';
|
||||
case 3:
|
||||
return 'B2I';
|
||||
case 5:
|
||||
return 'B1C';
|
||||
case 7:
|
||||
return 'B2a';
|
||||
default:
|
||||
return 'BDS sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.qzss:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'L1 C/A';
|
||||
case 4:
|
||||
return 'L1S';
|
||||
case 5:
|
||||
return 'L2C';
|
||||
case 6:
|
||||
return 'L5';
|
||||
default:
|
||||
return 'QZSS sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.sbas:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'L1';
|
||||
case 6:
|
||||
return 'L5';
|
||||
default:
|
||||
return 'SBAS sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.navic:
|
||||
switch (signalId) {
|
||||
case 1:
|
||||
return 'L5';
|
||||
case 2:
|
||||
return 'S';
|
||||
default:
|
||||
return 'NavIC sig $signalId';
|
||||
}
|
||||
|
||||
case GnssConstellation.mixed:
|
||||
return 'Mixed sig $signalId';
|
||||
|
||||
case GnssConstellation.unknown:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Kitűzési pont állapota.
|
||||
enum StakeoutStatus { pending, staked, skipped }
|
||||
|
||||
/// Kitűzendő pont — szeizmikus line/station modellel.
|
||||
///
|
||||
/// A pont természetes azonosítója a (lineId, station) páros. A rekord a
|
||||
/// TERV-koordinátákat ÉS a kitűzéskori MÉRT pozíciót is tárolja, az
|
||||
/// inline/crossline eltérésekkel együtt — így az export nemcsak azt
|
||||
/// dokumentálja, MIT tűztünk ki, hanem azt is, MILYEN pontossággal.
|
||||
///
|
||||
/// A séma szinkron-kész (uuid, updated_at, deleted_at, sync_status,
|
||||
/// created_by, device_id) — a Supabase-oldal a 4. ütemben épül rá.
|
||||
class StakeoutPoint {
|
||||
final int? id;
|
||||
final String uuid;
|
||||
final int projectId;
|
||||
|
||||
// ── Identitás ────────────────────────────────────────────────────
|
||||
final String lineId; // szeizmikus vonal azonosító ('' = vonal nélküli)
|
||||
final int station; // numerikus állomásszám (kötelező!)
|
||||
final String name; // megjelenítendő név (alapból a station szövege)
|
||||
final String pointType; // geofon / forras / egyeb — SPS-re előkészítve
|
||||
final String source; // csv / geojson / supabase / offset / kezi
|
||||
|
||||
// ── Terv-koordináták (mindkét rendszerben tárolva) ───────────────
|
||||
final double planEovY;
|
||||
final double planEovX;
|
||||
final double? planEovZ;
|
||||
final double planLat;
|
||||
final double planLon;
|
||||
|
||||
// ── Állapot + mért adatok ────────────────────────────────────────
|
||||
final StakeoutStatus status;
|
||||
final double? measuredEovY;
|
||||
final double? measuredEovX;
|
||||
final double? measuredEovZ;
|
||||
final double? measuredLat;
|
||||
final double? measuredLon;
|
||||
final double?
|
||||
devInline; // vonal menti eltérés (m, + = station-növekedés felé)
|
||||
final double? devCrossline; // vonalra merőleges eltérés (m, + = jobbra)
|
||||
final double? devDz; // magassági eltérés (m, terv − mért)
|
||||
final int? fixQuality; // GGA quality tároláskor (4 = RTK fixed)
|
||||
final double? accuracy; // vízszintes hiba tároláskor (m)
|
||||
final double? tiltDeg; // rúd-dőlés tároláskor (később: libella-modul)
|
||||
final DateTime? stakedAt;
|
||||
|
||||
// ── Eltolt (transzverzális) pont ─────────────────────────────────
|
||||
final bool isOffset;
|
||||
final String? parentUuid; // az eredeti tervpont uuid-ja
|
||||
final double? offsetDist; // eltolás nagysága (m)
|
||||
final double? offsetBearing; // eltolás iránya (fok, EOV-észak = 0)
|
||||
|
||||
// ── Szinkron / audit ─────────────────────────────────────────────
|
||||
final String? createdBy;
|
||||
final String? deviceId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final String syncStatus;
|
||||
|
||||
StakeoutPoint({
|
||||
this.id,
|
||||
String? uuid,
|
||||
required this.projectId,
|
||||
this.lineId = '',
|
||||
required this.station,
|
||||
String? name,
|
||||
this.pointType = 'geofon',
|
||||
this.source = 'csv',
|
||||
required this.planEovY,
|
||||
required this.planEovX,
|
||||
this.planEovZ,
|
||||
required this.planLat,
|
||||
required this.planLon,
|
||||
this.status = StakeoutStatus.pending,
|
||||
this.measuredEovY,
|
||||
this.measuredEovX,
|
||||
this.measuredEovZ,
|
||||
this.measuredLat,
|
||||
this.measuredLon,
|
||||
this.devInline,
|
||||
this.devCrossline,
|
||||
this.devDz,
|
||||
this.fixQuality,
|
||||
this.accuracy,
|
||||
this.tiltDeg,
|
||||
this.stakedAt,
|
||||
this.isOffset = false,
|
||||
this.parentUuid,
|
||||
this.offsetDist,
|
||||
this.offsetBearing,
|
||||
this.createdBy,
|
||||
this.deviceId,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
this.deletedAt,
|
||||
this.syncStatus = 'pending',
|
||||
}) : uuid = uuid ?? const Uuid().v4(),
|
||||
name = name ?? station.toString(),
|
||||
createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
/// Teljes azonosító megjelenítésre: "L1024 · 105" vagy csak "105".
|
||||
String get displayId => lineId.isEmpty ? name : '$lineId · $name';
|
||||
|
||||
StakeoutPoint copyWith({
|
||||
int? id,
|
||||
StakeoutStatus? status,
|
||||
double? measuredEovY,
|
||||
double? measuredEovX,
|
||||
double? measuredEovZ,
|
||||
double? measuredLat,
|
||||
double? measuredLon,
|
||||
double? devInline,
|
||||
double? devCrossline,
|
||||
double? devDz,
|
||||
int? fixQuality,
|
||||
double? accuracy,
|
||||
double? tiltDeg,
|
||||
DateTime? stakedAt,
|
||||
DateTime? updatedAt,
|
||||
String? syncStatus,
|
||||
}) {
|
||||
return StakeoutPoint(
|
||||
id: id ?? this.id,
|
||||
uuid: uuid,
|
||||
projectId: projectId,
|
||||
lineId: lineId,
|
||||
station: station,
|
||||
name: name,
|
||||
pointType: pointType,
|
||||
source: source,
|
||||
planEovY: planEovY,
|
||||
planEovX: planEovX,
|
||||
planEovZ: planEovZ,
|
||||
planLat: planLat,
|
||||
planLon: planLon,
|
||||
status: status ?? this.status,
|
||||
measuredEovY: measuredEovY ?? this.measuredEovY,
|
||||
measuredEovX: measuredEovX ?? this.measuredEovX,
|
||||
measuredEovZ: measuredEovZ ?? this.measuredEovZ,
|
||||
measuredLat: measuredLat ?? this.measuredLat,
|
||||
measuredLon: measuredLon ?? this.measuredLon,
|
||||
devInline: devInline ?? this.devInline,
|
||||
devCrossline: devCrossline ?? this.devCrossline,
|
||||
devDz: devDz ?? this.devDz,
|
||||
fixQuality: fixQuality ?? this.fixQuality,
|
||||
accuracy: accuracy ?? this.accuracy,
|
||||
tiltDeg: tiltDeg ?? this.tiltDeg,
|
||||
stakedAt: stakedAt ?? this.stakedAt,
|
||||
isOffset: isOffset,
|
||||
parentUuid: parentUuid,
|
||||
offsetDist: offsetDist,
|
||||
offsetBearing: offsetBearing,
|
||||
createdBy: createdBy,
|
||||
deviceId: deviceId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? DateTime.now(),
|
||||
deletedAt: deletedAt,
|
||||
syncStatus: syncStatus ?? 'pending',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'uuid': uuid,
|
||||
'project_id': projectId,
|
||||
'line_id': lineId,
|
||||
'station': station,
|
||||
'name': name,
|
||||
'point_type': pointType,
|
||||
'source': source,
|
||||
'plan_eov_y': planEovY,
|
||||
'plan_eov_x': planEovX,
|
||||
'plan_eov_z': planEovZ,
|
||||
'plan_lat': planLat,
|
||||
'plan_lon': planLon,
|
||||
'status': status.name,
|
||||
'measured_eov_y': measuredEovY,
|
||||
'measured_eov_x': measuredEovX,
|
||||
'measured_eov_z': measuredEovZ,
|
||||
'measured_lat': measuredLat,
|
||||
'measured_lon': measuredLon,
|
||||
'dev_inline': devInline,
|
||||
'dev_crossline': devCrossline,
|
||||
'dev_dz': devDz,
|
||||
'fix_quality': fixQuality,
|
||||
'accuracy': accuracy,
|
||||
'tilt_deg': tiltDeg,
|
||||
'staked_at': stakedAt?.toIso8601String(),
|
||||
'is_offset': isOffset ? 1 : 0,
|
||||
'parent_uuid': parentUuid,
|
||||
'offset_dist': offsetDist,
|
||||
'offset_bearing': offsetBearing,
|
||||
'created_by': createdBy,
|
||||
'device_id': deviceId,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt?.toIso8601String(),
|
||||
'sync_status': syncStatus,
|
||||
};
|
||||
|
||||
factory StakeoutPoint.fromMap(Map<String, dynamic> m) => StakeoutPoint(
|
||||
id: m['id'] as int?,
|
||||
uuid: m['uuid'] as String,
|
||||
projectId: m['project_id'] as int,
|
||||
lineId: (m['line_id'] as String?) ?? '',
|
||||
station: m['station'] as int,
|
||||
name: (m['name'] as String?) ?? '${m['station']}',
|
||||
pointType: (m['point_type'] as String?) ?? 'geofon',
|
||||
source: (m['source'] as String?) ?? 'csv',
|
||||
planEovY: (m['plan_eov_y'] as num).toDouble(),
|
||||
planEovX: (m['plan_eov_x'] as num).toDouble(),
|
||||
planEovZ: (m['plan_eov_z'] as num?)?.toDouble(),
|
||||
planLat: (m['plan_lat'] as num).toDouble(),
|
||||
planLon: (m['plan_lon'] as num).toDouble(),
|
||||
status: StakeoutStatus.values.firstWhere((s) => s.name == m['status'],
|
||||
orElse: () => StakeoutStatus.pending),
|
||||
measuredEovY: (m['measured_eov_y'] as num?)?.toDouble(),
|
||||
measuredEovX: (m['measured_eov_x'] as num?)?.toDouble(),
|
||||
measuredEovZ: (m['measured_eov_z'] as num?)?.toDouble(),
|
||||
measuredLat: (m['measured_lat'] as num?)?.toDouble(),
|
||||
measuredLon: (m['measured_lon'] as num?)?.toDouble(),
|
||||
devInline: (m['dev_inline'] as num?)?.toDouble(),
|
||||
devCrossline: (m['dev_crossline'] as num?)?.toDouble(),
|
||||
devDz: (m['dev_dz'] as num?)?.toDouble(),
|
||||
fixQuality: m['fix_quality'] as int?,
|
||||
accuracy: (m['accuracy'] as num?)?.toDouble(),
|
||||
tiltDeg: (m['tilt_deg'] as num?)?.toDouble(),
|
||||
stakedAt: m['staked_at'] != null
|
||||
? DateTime.tryParse(m['staked_at'] as String)
|
||||
: null,
|
||||
isOffset: (m['is_offset'] as int? ?? 0) == 1,
|
||||
parentUuid: m['parent_uuid'] as String?,
|
||||
offsetDist: (m['offset_dist'] as num?)?.toDouble(),
|
||||
offsetBearing: (m['offset_bearing'] as num?)?.toDouble(),
|
||||
createdBy: m['created_by'] as String?,
|
||||
deviceId: m['device_id'] as String?,
|
||||
createdAt: DateTime.tryParse((m['created_at'] as String?) ?? '') ??
|
||||
DateTime.now(),
|
||||
updatedAt: DateTime.tryParse((m['updated_at'] as String?) ?? '') ??
|
||||
DateTime.now(),
|
||||
deletedAt: m['deleted_at'] != null
|
||||
? DateTime.tryParse(m['deleted_at'] as String)
|
||||
: null,
|
||||
syncStatus: (m['sync_status'] as String?) ?? 'pending',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
// ─── TrackPoint ───────────────────────────────────────────────────────────────
|
||||
|
||||
class TrackPoint {
|
||||
final int? id;
|
||||
final int trackId;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double? altitude;
|
||||
final double? accuracy;
|
||||
final double? speed; // m/s
|
||||
final double? heading;
|
||||
final DateTime timestamp;
|
||||
|
||||
const TrackPoint({
|
||||
this.id,
|
||||
required this.trackId,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.altitude,
|
||||
this.accuracy,
|
||||
this.speed,
|
||||
this.heading,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
'track_id': trackId,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'altitude': altitude,
|
||||
'accuracy': accuracy,
|
||||
'speed': speed,
|
||||
'heading': heading,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
factory TrackPoint.fromMap(Map<String, dynamic> m) => TrackPoint(
|
||||
id: m['id'] as int?,
|
||||
trackId: m['track_id'] as int,
|
||||
latitude: m['latitude'] as double,
|
||||
longitude: m['longitude'] as double,
|
||||
altitude: m['altitude'] as double?,
|
||||
accuracy: m['accuracy'] as double?,
|
||||
speed: m['speed'] as double?,
|
||||
heading: m['heading'] as double?,
|
||||
timestamp: DateTime.parse(m['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Track státusz ────────────────────────────────────────────────────────────
|
||||
|
||||
enum TrackStatus { recording, paused, finished }
|
||||
|
||||
enum TrackSyncMode { localOnly, online }
|
||||
|
||||
enum TrackSyncStatus { pending, synced, error }
|
||||
// ─── Track ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class Track {
|
||||
final int? id;
|
||||
final String? uuid; // kliens-generált uuid — ez a szerveroldali id is
|
||||
final int? projectId;
|
||||
final String name;
|
||||
final DateTime startTime;
|
||||
final DateTime? endTime;
|
||||
final TrackStatus status;
|
||||
final String source; // pl. "Telefon GPS", "BLE GNSS"
|
||||
|
||||
// Statisztikák — ezeket a DB is tárolja a gyors listázáshoz
|
||||
final double distanceMeters;
|
||||
final int pointCount;
|
||||
final TrackSyncMode syncMode;
|
||||
final TrackSyncStatus syncStatus;
|
||||
final String? supabaseId;
|
||||
|
||||
const Track(
|
||||
{this.id,
|
||||
this.uuid,
|
||||
this.projectId,
|
||||
required this.name,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
this.status = TrackStatus.recording,
|
||||
this.source = 'Telefon GPS',
|
||||
this.distanceMeters = 0,
|
||||
this.pointCount = 0,
|
||||
this.syncMode = TrackSyncMode.online,
|
||||
this.syncStatus = TrackSyncStatus.pending,
|
||||
this.supabaseId});
|
||||
|
||||
Track copyWith(
|
||||
{int? id,
|
||||
String? uuid,
|
||||
int? projectId,
|
||||
String? name,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
TrackStatus? status,
|
||||
String? source,
|
||||
double? distanceMeters,
|
||||
int? pointCount,
|
||||
TrackSyncMode? syncMode,
|
||||
TrackSyncStatus? syncStatus,
|
||||
String? supabaseId}) =>
|
||||
Track(
|
||||
id: id ?? this.id,
|
||||
uuid: uuid ?? this.uuid,
|
||||
projectId: projectId ?? this.projectId,
|
||||
name: name ?? this.name,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
status: status ?? this.status,
|
||||
source: source ?? this.source,
|
||||
distanceMeters: distanceMeters ?? this.distanceMeters,
|
||||
pointCount: pointCount ?? this.pointCount,
|
||||
syncMode: syncMode ?? this.syncMode,
|
||||
syncStatus: syncStatus ?? this.syncStatus,
|
||||
supabaseId: supabaseId ?? this.supabaseId);
|
||||
|
||||
/// Formázott időtartam (óó:pp:mm)
|
||||
String get durationFormatted {
|
||||
final end = endTime ?? DateTime.now();
|
||||
final d = end.difference(startTime);
|
||||
final h = d.inHours.toString().padLeft(2, '0');
|
||||
final m = (d.inMinutes % 60).toString().padLeft(2, '0');
|
||||
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
|
||||
return '$h:$m:$s';
|
||||
}
|
||||
|
||||
/// Formázott távolság
|
||||
String get distanceFormatted {
|
||||
if (distanceMeters < 1000) {
|
||||
return '${distanceMeters.toStringAsFixed(0)} m';
|
||||
}
|
||||
return '${(distanceMeters / 1000).toStringAsFixed(2)} km';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
if (uuid != null) 'uuid': uuid,
|
||||
if (projectId != null) 'project_id': projectId,
|
||||
'name': name,
|
||||
'start_time': startTime.toIso8601String(),
|
||||
'end_time': endTime?.toIso8601String(),
|
||||
'status': status.name,
|
||||
'source': source,
|
||||
'distance_m': distanceMeters,
|
||||
'point_count': pointCount,
|
||||
'is_local_only': syncMode == TrackSyncMode.localOnly ? 1 : 0,
|
||||
'sync_status': syncStatus.name,
|
||||
'supabase_id': supabaseId,
|
||||
};
|
||||
|
||||
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
||||
id: m['id'] as int?,
|
||||
uuid: m['uuid'] as String?,
|
||||
projectId: m['project_id'] as int?,
|
||||
name: m['name'] as String,
|
||||
startTime: DateTime.parse(m['start_time'] as String),
|
||||
endTime: m['end_time'] != null
|
||||
? DateTime.parse(m['end_time'] as String)
|
||||
: null,
|
||||
status: TrackStatus.values.firstWhere(
|
||||
(s) => s.name == (m['status'] as String?),
|
||||
orElse: () => TrackStatus.finished,
|
||||
),
|
||||
source: m['source'] as String? ?? 'Telefon GPS',
|
||||
distanceMeters: (m['distance_m'] as num?)?.toDouble() ?? 0,
|
||||
pointCount: m['point_count'] as int? ?? 0,
|
||||
syncMode: (m['is_local_only'] as int?) == 1
|
||||
? TrackSyncMode.localOnly
|
||||
: TrackSyncMode.online,
|
||||
syncStatus: TrackSyncStatus.values
|
||||
.byName(m['sync_status'] as String? ?? 'pending'),
|
||||
supabaseId: m['supabase_id'] as String?);
|
||||
|
||||
bool get isLocalOnly => syncMode == TrackSyncMode.localOnly;
|
||||
}
|
||||
|
||||
// ─── Segédszámítás: Haversine távolság ───────────────────────────────────────
|
||||
|
||||
double haversineMeters(double lat1, double lon1, double lat2, double lon2) {
|
||||
const r = 6371000.0;
|
||||
final dLat = _toRad(lat2 - lat1);
|
||||
final dLon = _toRad(lon2 - lon1);
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(_toRad(lat1)) *
|
||||
math.cos(_toRad(lat2)) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
double _toRad(double deg) => deg * math.pi / 180;
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
|
||||
import '../../../../services/contact_service.dart';
|
||||
|
||||
class ContactsController extends GetxController {
|
||||
final items = <ContactWithState>[].obs;
|
||||
final isLoading = false.obs;
|
||||
final error = ''.obs;
|
||||
final search = ''.obs;
|
||||
final pendingCount = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
load();
|
||||
}
|
||||
|
||||
List<ContactWithState> get filtered {
|
||||
final q = search.value.trim().toLowerCase();
|
||||
if (q.isEmpty) return items;
|
||||
return items.where((it) {
|
||||
final c = it.contact;
|
||||
return c.name.toLowerCase().contains(q) ||
|
||||
c.phone.toLowerCase().contains(q) ||
|
||||
c.email.toLowerCase().contains(q) ||
|
||||
c.address.toLowerCase().contains(q);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<void> load({bool silent = false}) async {
|
||||
final projectId = ProjectService.to.activeProject.value?.uuid;
|
||||
if (projectId == null) {
|
||||
error.value = 'Nincs aktív projekt.';
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
if (!silent) isLoading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
items.value = await ContactService.to.listMerged(projectId);
|
||||
pendingCount.value = await ContactService.to.pendingCount(projectId);
|
||||
} catch (e) {
|
||||
error.value = _friendly(e);
|
||||
} finally {
|
||||
if (!silent) isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Feltölti a várólistát, majd újratölt (kézi "szinkron most" gomb).
|
||||
Future<void> flushNow() async {
|
||||
try {
|
||||
final n = await ContactService.to.flush();
|
||||
if (n > 0) {
|
||||
Get.snackbar('Szinkron', '$n kapcsolat feltöltve.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
await load();
|
||||
} catch (e) {
|
||||
Get.snackbar('Szinkron', _friendly(e),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(ContactWithState it) async {
|
||||
try {
|
||||
await ContactService.to
|
||||
.delete(id: it.contact.id, localUuid: it.localUuid);
|
||||
items.remove(it);
|
||||
final projectId = ProjectService.to.activeProject.value?.uuid;
|
||||
if (projectId != null) {
|
||||
pendingCount.value = await ContactService.to.pendingCount(projectId);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Hiba', _friendly(e), snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendly(Object e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('permission') ||
|
||||
s.contains('policy') ||
|
||||
s.contains('row-level')) {
|
||||
return 'Nincs jogosultságod a kapcsolatok eléréséhez.';
|
||||
}
|
||||
if (s.contains('SocketException') || s.contains('Failed host')) {
|
||||
return 'Nincs hálózati kapcsolat.';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/contacts/presentation/controllers/contacts_controller.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
|
||||
import '../../../../models/contact.dart';
|
||||
import '../../../../services/contact_service.dart';
|
||||
|
||||
/// Kapcsolat szerkesztő — TELJES OLDAL (Scaffold), nem bottom sheet.
|
||||
///
|
||||
/// Miért oldal: a bottom sheet a szoftveres billentyűzettel rosszul
|
||||
/// viselkedik (a mezőket eltakarja / a lapot feltolja). A Scaffold body
|
||||
/// automatikusan a billentyűzet fölé görget, a mentés gomb pedig fixen
|
||||
/// az appbarban van — kesztyűs, terepi használatra is kényelmes.
|
||||
///
|
||||
/// Új rekord: `Get.to(() => const ContactEditView())`.
|
||||
/// Szerkesztés: `Get.to(() => const ContactEditView(), arguments: contact)`.
|
||||
class ContactEditView extends StatefulWidget {
|
||||
const ContactEditView({super.key});
|
||||
|
||||
@override
|
||||
State<ContactEditView> createState() => _ContactEditViewState();
|
||||
}
|
||||
|
||||
class _ContactEditViewState extends State<ContactEditView> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final Contact? _original;
|
||||
|
||||
late final TextEditingController _name;
|
||||
late final TextEditingController _address;
|
||||
late final TextEditingController _phone;
|
||||
late final TextEditingController _email;
|
||||
late final TextEditingController _note;
|
||||
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_original = Get.arguments is Contact ? Get.arguments as Contact : null;
|
||||
_name = TextEditingController(text: _original?.name ?? '');
|
||||
_address = TextEditingController(text: _original?.address ?? '');
|
||||
_phone = TextEditingController(text: _original?.phone ?? '');
|
||||
_email = TextEditingController(text: _original?.email ?? '');
|
||||
_note = TextEditingController(text: _original?.note ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_name.dispose();
|
||||
_address.dispose();
|
||||
_phone.dispose();
|
||||
_email.dispose();
|
||||
_note.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final contact = (_original ??
|
||||
Contact(
|
||||
name: '',
|
||||
projectId: ProjectService.to.activeProject.value!.uuid))
|
||||
.copyWith(
|
||||
name: _name.text,
|
||||
address: _address.text,
|
||||
phone: _phone.text,
|
||||
email: _email.text,
|
||||
note: _note.text,
|
||||
);
|
||||
final queued = await ContactService.to.save(contact);
|
||||
Get.back(result: queued); // a lista frissítéshez visszakapja
|
||||
if (Get.isRegistered<ContactsController>()) {
|
||||
Get.find<ContactsController>().load(silent: true);
|
||||
}
|
||||
Get.snackbar(queued ? 'Elmentve (offline)' : 'Mentve',
|
||||
queued ? '${contact.name} - feltöltés, amint van net' : contact.name,
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
} catch (e) {
|
||||
Get.snackbar('Hiba', _friendly(e),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
colorText: const Color(0xFFFFFFFF));
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendly(Object e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('permission') ||
|
||||
s.contains('policy') ||
|
||||
s.contains('row-level')) {
|
||||
return 'Nincs jogosultságod a művelethez.';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isNew = _original?.id == null;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'Új kapcsolat' : 'Kapcsolat szerkesztése'),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: _saving
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.check),
|
||||
label: const Text('Mentés'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _name,
|
||||
autofocus: isNew,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Név *',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) => (v == null || v.trim().isEmpty)
|
||||
? 'A név megadása kötelező.'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _address,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Cím',
|
||||
prefixIcon: Icon(Icons.location_on_outlined),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Telefonszám',
|
||||
prefixIcon: Icon(Icons.phone_outlined),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'E-mail',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
final ok =
|
||||
RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(v.trim());
|
||||
return ok ? null : 'Érvénytelen e-mail cím.';
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _note,
|
||||
maxLines: 4,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Megjegyzés',
|
||||
prefixIcon: Icon(Icons.notes_outlined),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
label: const Text('Mentés'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/models/project.dart';
|
||||
import 'package:terepi_seged/services/contact_export_service.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../models/contact.dart';
|
||||
import '../../../../services/contact_service.dart';
|
||||
import '../../../../services/permission_service.dart';
|
||||
import '../controllers/contacts_controller.dart';
|
||||
import 'contact_edit_view.dart';
|
||||
|
||||
/// Kapcsolatok lista — csak bejelentkezett ÉS jogosult felhasználóknak.
|
||||
///
|
||||
/// A jogosultság-ellenőrzés kétszintű: a UI a PermissionService-szel
|
||||
/// elrejti/tiltja az oldalt (barátságos üzenet), a tényleges védelmet
|
||||
/// pedig a Supabase-oldali RLS adja (az RLS-elutasítás is kezelt).
|
||||
class ContactsView extends StatelessWidget {
|
||||
const ContactsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Jogosultság-kapu — ha nincs joga, be sem töltjük a listát.
|
||||
if (Get.isRegistered<PermissionService>() &&
|
||||
!PermissionService.to.canContacts) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Kapcsolatok')),
|
||||
body: const _NoAccess(),
|
||||
);
|
||||
}
|
||||
// Projekt-kapu — a kapcsolat mindig egy projekthez tartozik, és csak
|
||||
// MEGOSZTOTT (online) projektnél van értelme (a funkció a Supabase-en
|
||||
// él). Csak-lokális vagy hiányzó projektnél blokkolunk.
|
||||
final activeProject = ProjectService.to.activeProject.value;
|
||||
if (activeProject == null || activeProject.isLocalOnly) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Kapcsolatok')),
|
||||
body: _ProjectRequired(project: activeProject),
|
||||
);
|
||||
}
|
||||
|
||||
final c = Get.put(ContactsController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Kapcsolatok'),
|
||||
Text(activeProject.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.normal)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.upload_file),
|
||||
tooltip: 'Import CSV',
|
||||
onPressed: () => _importCsv(c),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.ios_share),
|
||||
tooltip: 'Export CSV',
|
||||
onPressed: () => _exportDialog(c, activeProject.name),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Frissítés',
|
||||
onPressed: c.load,
|
||||
),
|
||||
Obx(() => c.pendingCount.value == 0
|
||||
? const SizedBox.shrink()
|
||||
: IconButton(
|
||||
icon: Badge(
|
||||
label: Text('${c.pendingCount.value}'),
|
||||
child: const Icon(Icons.cloud_upload_outlined),
|
||||
),
|
||||
tooltip: 'Várólista feltöltése',
|
||||
onPressed: c.flushNow,
|
||||
))
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final saved = await Get.to(() => const ContactEditView());
|
||||
if (saved is Contact) c.load();
|
||||
},
|
||||
icon: const Icon(Icons.person_add_alt),
|
||||
label: const Text('Új'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Keresés név, telefon, e-mail…',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (v) => c.search.value = v,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (c.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (c.error.value.isNotEmpty) {
|
||||
return _ErrorState(message: c.error.value, onRetry: c.load);
|
||||
}
|
||||
final rows = c.filtered;
|
||||
if (rows.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Nincs megjeleníthető kapcsolat.'));
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: rows.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) => _ContactTile(
|
||||
item: rows[i],
|
||||
onEdit: () async {
|
||||
final saved = await Get.to(() => const ContactEditView(),
|
||||
arguments: rows[i].contact);
|
||||
if (saved != null) c.load();
|
||||
},
|
||||
onDelete: () => _confirmDelete(c, rows[i]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(
|
||||
ContactsController c, ContactWithState item) async {
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Törlés'),
|
||||
content: Text('Biztosan törlöd: ${item.contact.name}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Törlés')),
|
||||
],
|
||||
));
|
||||
if (ok == true) await c.delete(item);
|
||||
}
|
||||
|
||||
Future<void> _importCsv(ContactsController c) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
final picked = result?.files.single;
|
||||
final path = picked?.path;
|
||||
if (path == null) return;
|
||||
|
||||
final name = picked!.name.toLowerCase();
|
||||
if (!(name.endsWith('.csv') || name.endsWith('.txt'))) {
|
||||
Get.snackbar('Import', 'CSV vagy TXT fájlt válassz.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
return;
|
||||
}
|
||||
final activeProject = ProjectService.to.activeProject.value;
|
||||
final parsed = await ContactService.parseCsv(File(path),
|
||||
projectId: activeProject!.uuid);
|
||||
if (parsed.isEmpty) {
|
||||
Get.snackbar('Import', 'Nem található érvényes sor a fájlban.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
return;
|
||||
}
|
||||
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Import megerősítése'),
|
||||
content: Text('${parsed.length} kapcsolat importálása?\n\n'
|
||||
'Első néhány: '
|
||||
'${parsed.take(3).map((e) => e.name).join(', ')}…'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Import')),
|
||||
],
|
||||
));
|
||||
if (ok != true) return;
|
||||
|
||||
final imported = await ContactService.to.importMany(parsed);
|
||||
await c.load();
|
||||
Get.snackbar(
|
||||
'Import kész',
|
||||
imported.queued > 0
|
||||
? '${imported.uploaded} feltöltve, ${imported.queued} offline '
|
||||
'várólistán (net jöttével automatikusan felmegy).'
|
||||
: '${imported.uploaded} kapcsolat importálva.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Import hiba', e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportDialog(ContactsController c, String projectName) async {
|
||||
final hungarian = true.obs;
|
||||
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Export CSV'),
|
||||
content: Obx(() => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Magyar Excel-formátum'),
|
||||
subtitle: const Text('pontosvessző elválasztó',
|
||||
style: TextStyle(fontSize: 11)),
|
||||
value: hungarian.value,
|
||||
onChanged: (v) => hungarian.value = v,
|
||||
)),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.ios_share, size: 18),
|
||||
label: const Text('Export'),
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
try {
|
||||
await ContactExportService.exportAndShare(
|
||||
contacts: c.filtered.map((it) => it.contact).toList(),
|
||||
// contacts: c.items.map((it) => it.contact).toList(),
|
||||
hungarian: hungarian.value,
|
||||
projectName: projectName,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Export', e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sor ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _ContactTile extends StatelessWidget {
|
||||
final ContactWithState item;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
const _ContactTile(
|
||||
{required this.item, required this.onEdit, required this.onDelete});
|
||||
|
||||
Contact get contact => item.contact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = [
|
||||
if (contact.phone.isNotEmpty) contact.phone,
|
||||
if (contact.email.isNotEmpty) contact.email,
|
||||
].join(' · ');
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
child:
|
||||
Text(contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?'),
|
||||
),
|
||||
title: Text(contact.name),
|
||||
subtitle: subtitle.isEmpty ? null : Text(subtitle),
|
||||
onTap: () => _showDetail(context),
|
||||
trailing: item.isPending
|
||||
? const Tooltip(
|
||||
message: 'Feltöltésre vár',
|
||||
child: Icon(Icons.cloud_upload_outlined,
|
||||
size: 20, color: Colors.orange))
|
||||
: PopupMenuButton<String>(
|
||||
onSelected: (a) {
|
||||
switch (a) {
|
||||
case 'edit':
|
||||
onEdit();
|
||||
case 'delete':
|
||||
onDelete();
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(value: 'edit', child: Text('Szerkesztés')),
|
||||
PopupMenuItem(value: 'delete', child: Text('Törlés')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetail(BuildContext context) {
|
||||
Get.dialog(AlertDialog(
|
||||
title: Text(contact.name),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (contact.address.isNotEmpty)
|
||||
_row(Icons.location_on_outlined, contact.address),
|
||||
if (contact.phone.isNotEmpty)
|
||||
_row(Icons.phone_outlined, contact.phone,
|
||||
onTap: () => _launch('tel:${contact.phone}')),
|
||||
if (contact.email.isNotEmpty)
|
||||
_row(Icons.email_outlined, contact.email,
|
||||
onTap: () => _launch('mailto:${contact.email}')),
|
||||
if (contact.note.isNotEmpty) _row(Icons.notes_outlined, contact.note),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onEdit();
|
||||
},
|
||||
child: const Text('Szerkesztés')),
|
||||
TextButton(onPressed: Get.back, child: const Text('Bezár')),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _row(IconData icon, String text, {VoidCallback? onTap}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Colors.grey),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
color: onTap != null ? Colors.blue : null,
|
||||
decoration: onTap != null ? TextDecoration.underline : null,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launch(String uri) async {
|
||||
final u = Uri.parse(uri);
|
||||
if (await canLaunchUrl(u)) await launchUrl(u);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Állapot-widgetek ─────────────────────────────────────────────────
|
||||
|
||||
class _NoAccess extends StatelessWidget {
|
||||
const _NoAccess();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.lock_outline, size: 48, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Nincs jogosultságod a kapcsolatok eléréséhez.\n'
|
||||
'Kérj hozzáférést a rendszergazdától.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorState extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback onRetry;
|
||||
const _ErrorState({required this.message, required this.onRetry});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.cloud_off, size: 44, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
Text(message, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(onPressed: onRetry, child: const Text('Újra')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProjectRequired extends StatelessWidget {
|
||||
final Project? project;
|
||||
const _ProjectRequired({required this.project});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLocalOnly = project?.isLocalOnly ?? false;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.folder_off_outlined,
|
||||
size: 48, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
project == null
|
||||
? 'Nincs aktív projekt.\nVálassz egyet a menüből.'
|
||||
: 'A(z) "${project!.name}" projekt csak lokális.\n'
|
||||
'A kapcsolatok megosztott (online) projekthez '
|
||||
'tartoznak — válts megosztott projektre, vagy tedd '
|
||||
'megosztottá ezt.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/field_trip/presentations/controllers/field_trip_controller.dart';
|
||||
|
||||
class FieldTripBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => FieldTripController());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class FieldTripController extends GetxController {
|
||||
RxBool mapIsInitialized = false.obs;
|
||||
RxBool mapInEditorMode = false.obs;
|
||||
RxBool mapInPointEditorMode = false.obs;
|
||||
|
||||
final pointNotes = <Marker>[].obs;
|
||||
final polylineNotes = <Polyline<Object>>[].obs;
|
||||
final polygonNotes = <Polygon<Object>>[].obs;
|
||||
|
||||
late final MapController mapController;
|
||||
late final MapOptions mapOptions;
|
||||
late final PolygonEditorController polygonEditorController;
|
||||
|
||||
final PolygonLabelPlacementCalculator _labelPlacementCalculator =
|
||||
const PolygonLabelPlacementCalculator.centroid();
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// TODO: implement onInit
|
||||
super.onInit();
|
||||
|
||||
LatLng initialCenter = const LatLng(47.5, 19.0);
|
||||
try {
|
||||
final last = await Geolocator.getLastKnownPosition();
|
||||
if (last != null) {
|
||||
initialCenter = LatLng(last.latitude, last.longitude);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
mapOptions = MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: 13,
|
||||
onLongPress: (tapPosition, point) {
|
||||
if (mapInEditorMode.value) {
|
||||
polygonEditorController.addPoint(point);
|
||||
}
|
||||
if (mapInPointEditorMode.value) {
|
||||
savePointNote(point: point);
|
||||
}
|
||||
},
|
||||
keepAlive: true,
|
||||
);
|
||||
|
||||
polylineNotes.add(
|
||||
Polyline(
|
||||
points: [
|
||||
const LatLng(47.65, 19.00),
|
||||
const LatLng(47.64, 19.02),
|
||||
const LatLng(47.69, 19.22),
|
||||
],
|
||||
strokeWidth: 8,
|
||||
color: const Color.fromARGB(255, 227, 238, 71),
|
||||
hitValue: (
|
||||
title: 'Purple Line',
|
||||
subtitle: 'Nothing really special here...',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
polygonEditorController =
|
||||
PolygonEditorController(mode: PolygonEditorMode.polygon);
|
||||
|
||||
mapController = MapController();
|
||||
|
||||
mapIsInitialized.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
polygonEditorController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void cancelEditorController() {
|
||||
polygonEditorController.clear();
|
||||
mapInEditorMode.value = false;
|
||||
}
|
||||
|
||||
void startPolygonEdition() {
|
||||
polygonEditorController.clear();
|
||||
polygonEditorController.setMode(PolygonEditorMode.polygon);
|
||||
mapInEditorMode.value = true;
|
||||
}
|
||||
|
||||
void startPolylineEdition() {
|
||||
polygonEditorController.clear();
|
||||
polygonEditorController.setMode(PolygonEditorMode.line);
|
||||
mapInEditorMode.value = true;
|
||||
}
|
||||
|
||||
void startPointEdition() {
|
||||
mapInPointEditorMode.value = true;
|
||||
}
|
||||
|
||||
void saveNote() {
|
||||
if (polygonEditorController.mode == PolygonEditorMode.line) {
|
||||
print("Points number in line: ${polygonEditorController.points.length}");
|
||||
print(
|
||||
"1. point coords: ${polygonEditorController.points[0].latitude} - ${polygonEditorController.points[0].longitude}");
|
||||
if (polygonEditorController.points.length < 2) return;
|
||||
|
||||
Polyline polyline = Polyline(
|
||||
points: List.from(polygonEditorController.points),
|
||||
color: Colors.red,
|
||||
strokeWidth: 8,
|
||||
// hitValue: (
|
||||
// title: 'Purple Line',
|
||||
// subtitle: 'Nothing really special here...',
|
||||
// ),
|
||||
);
|
||||
polylineNotes.add(polyline);
|
||||
// polylineNotes.refresh();
|
||||
|
||||
print("Points number in polylineNotes: ${polylineNotes.length}");
|
||||
print(
|
||||
"1. point coords of polyline: ${polyline.points[0].latitude} - ${polyline.points[0].longitude}");
|
||||
|
||||
polygonEditorController.clear();
|
||||
mapInEditorMode.value = false;
|
||||
}
|
||||
if (polygonEditorController.mode == PolygonEditorMode.polygon) {
|
||||
print(
|
||||
"Points number in polygon: ${polygonEditorController.points.length}");
|
||||
|
||||
Polygon polygon = Polygon(
|
||||
points: List.from(polygonEditorController.points),
|
||||
color: Colors.purple,
|
||||
borderColor: Colors.yellow,
|
||||
borderStrokeWidth: 4,
|
||||
label: 'Label!',
|
||||
labelPlacementCalculator: _labelPlacementCalculator,
|
||||
// hitValue: (
|
||||
// title: 'Basic Filled Polygon',
|
||||
// subtitle: 'Nothing really special here...',
|
||||
);
|
||||
|
||||
polygonNotes.add(polygon);
|
||||
//polygonNotes.refresh();
|
||||
//update();
|
||||
|
||||
print("Points number in polygonNotes: ${polygonNotes.length}");
|
||||
|
||||
polygonEditorController.clear();
|
||||
mapInEditorMode.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void savePointNote({required LatLng point}) {
|
||||
Marker marker = Marker(
|
||||
point: point,
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: Container(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[700],
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(width: 1.0, color: Colors.black)),
|
||||
));
|
||||
pointNotes.add(marker);
|
||||
pointNotes.refresh();
|
||||
update();
|
||||
mapInPointEditorMode.value = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
|
||||
import 'package:flutter_map_polygon_editor/polygon_editor.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/pages/field_trip/presentations/controllers/field_trip_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FieldTripView extends GetView<FieldTripController> {
|
||||
const FieldTripView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
appBar: AppBar(
|
||||
title: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Terepbejárás'),
|
||||
],
|
||||
)),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(children: [
|
||||
Obx(() => controller.mapIsInitialized.value
|
||||
? FlutterMap(
|
||||
mapController: controller.mapController,
|
||||
options: controller.mapOptions,
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
|
||||
subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
|
||||
maxNativeZoom: 18,
|
||||
),
|
||||
CurrentLocationLayer(
|
||||
alignPositionOnUpdate: AlignOnUpdate.once,
|
||||
),
|
||||
Obx(() =>
|
||||
MarkerLayer(markers: [...controller.pointNotes])),
|
||||
Obx(() => PolylineLayer(
|
||||
polylines: [...controller.polylineNotes],
|
||||
)),
|
||||
Obx(() => PolygonLayer(
|
||||
polygons: [...controller.polygonNotes],
|
||||
useAltRendering: true,
|
||||
)),
|
||||
// PolylineLayer(polylines: [
|
||||
// Polyline(
|
||||
// points: [
|
||||
// const LatLng(47.65, 19.00),
|
||||
// const LatLng(47.64, 19.02),
|
||||
// const LatLng(47.69, 19.22),
|
||||
// ],
|
||||
// strokeWidth: 8,
|
||||
// color: const Color(0xFF60399E),
|
||||
// hitValue: (
|
||||
// title: 'Purple Line',
|
||||
// subtitle: 'Nothing really special here...',
|
||||
// ),
|
||||
// ),
|
||||
// ]),
|
||||
PolygonEditor(
|
||||
controller: controller.polygonEditorController,
|
||||
throttleDuration: Duration.zero)
|
||||
],
|
||||
)
|
||||
: const Center(child: CircularProgressIndicator())),
|
||||
Positioned(
|
||||
bottom: 3,
|
||||
right: 3,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filled(
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
controller.startPointEdition();
|
||||
},
|
||||
icon: const Icon(Icons.flag)),
|
||||
IconButton.filled(
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
controller.startPolylineEdition();
|
||||
},
|
||||
icon: const Icon(Icons.polyline)),
|
||||
IconButton.filled(
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
controller.startPolygonEdition();
|
||||
},
|
||||
icon: const Icon(Icons.border_outer)),
|
||||
IconButton.filled(
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
controller.saveNote();
|
||||
},
|
||||
icon: const Icon(Icons.done)),
|
||||
IconButton.filled(
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
controller.cancelEditorController();
|
||||
},
|
||||
icon: const Icon(Icons.cancel))
|
||||
],
|
||||
))
|
||||
]))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/home/presentation/controllers/home_controller.dart';
|
||||
import 'package:terepi_seged/pages/home/presentation/widgets/big_button_widget.dart';
|
||||
import 'package:terepi_seged/widgets/app_drawer.dart';
|
||||
|
||||
class HomeView extends GetView<HomeViewController> {
|
||||
const HomeView({Key? key}) : super(key: key);
|
||||
@@ -10,167 +11,179 @@ class HomeView extends GetView<HomeViewController> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
backgroundColor: Colors.black.withOpacity(0.05),
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
title: const Text('Kezdőlap'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Column(children: [
|
||||
Column(
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10.0),
|
||||
child: Center(
|
||||
child: Text("Kezdőlap"),
|
||||
),
|
||||
),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/map');
|
||||
// },
|
||||
// child: const Text('Map')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/bluetooth_test');
|
||||
// },
|
||||
// child: const Text('Bluetooth test')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/socket_test');
|
||||
// },
|
||||
// child: const Text('Socket teszt')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/rtcm_test');
|
||||
// },
|
||||
// child: const Text('RTCM teszt'))
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Get.toNamed("/field_trip");
|
||||
},
|
||||
child: Text('Field trip')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed("/tracking");
|
||||
// },
|
||||
// child: Text('Tracking')),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Get.toNamed("/map");
|
||||
},
|
||||
child: Text('Kitűzés'))
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// BigButtonWidget(
|
||||
// iconData: Icons.flag,
|
||||
// label: "Kitűzés",
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/map');
|
||||
// },
|
||||
// ),
|
||||
BigButtonWidget(
|
||||
iconData: Icons.flag,
|
||||
label: "Bemérés",
|
||||
onPressed: () async {
|
||||
Get.toNamed('/map_survey');
|
||||
},
|
||||
),
|
||||
BigButtonWidget(
|
||||
iconData: Icons.monitor_heart,
|
||||
label: "Mérés",
|
||||
onPressed: () async {
|
||||
Get.toNamed("/measured_data");
|
||||
// ScaffoldMessenger.of(context)
|
||||
// .showSnackBar(const SnackBar(
|
||||
// content: Text(
|
||||
// "Fejlesztlés alatt",
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// backgroundColor: Colors.black54,
|
||||
// ));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(children: [
|
||||
Column(
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10.0),
|
||||
child: Center(
|
||||
child: Text("Kezdőlap"),
|
||||
),
|
||||
),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/map');
|
||||
// },
|
||||
// child: const Text('Map')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/bluetooth_test');
|
||||
// },
|
||||
// child: const Text('Bluetooth test')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/socket_test');
|
||||
// },
|
||||
// child: const Text('Socket teszt')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// Get.toNamed('/rtcm_test');
|
||||
// },
|
||||
// child: const Text('RTCM teszt'))
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
BigButtonWidget(
|
||||
iconData: Icons.flag,
|
||||
label: "Kitűzés",
|
||||
onPressed: () async {
|
||||
Get.toNamed('/map');
|
||||
},
|
||||
),
|
||||
BigButtonWidget(
|
||||
iconData: Icons.monitor_heart,
|
||||
label: "Mérés",
|
||||
onPressed: () async {
|
||||
Get.toNamed("/measured_data");
|
||||
// ScaffoldMessenger.of(context)
|
||||
// .showSnackBar(const SnackBar(
|
||||
// content: Text(
|
||||
// "Fejlesztlés alatt",
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// backgroundColor: Colors.black54,
|
||||
// ));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
BigButtonWidget(
|
||||
iconData: Icons.navigation,
|
||||
label: "Navigáció",
|
||||
onPressed: () async {
|
||||
Get.toNamed('/navigation');
|
||||
// ScaffoldMessenger.of(context)
|
||||
// .showSnackBar(const SnackBar(
|
||||
// content: Text(
|
||||
// "Fejlesztlés alatt",
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// backgroundColor: Colors.black54,
|
||||
// ));
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
BigButtonWidget(
|
||||
iconData: Icons.navigation,
|
||||
label: "Navigáció",
|
||||
onPressed: () async {
|
||||
Get.toNamed('/navigation');
|
||||
// ScaffoldMessenger.of(context)
|
||||
// .showSnackBar(const SnackBar(
|
||||
// content: Text(
|
||||
// "Fejlesztlés alatt",
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// backgroundColor: Colors.black54,
|
||||
// ));
|
||||
},
|
||||
),
|
||||
BigButtonWidget(
|
||||
iconData: Icons.message,
|
||||
label: "Üzenetek",
|
||||
onPressed: () async {
|
||||
// Get.toNamed("/navigation");
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
BigButtonWidget(
|
||||
iconData: Icons.message,
|
||||
label: "Üzenetek",
|
||||
onPressed: () async {
|
||||
// Get.toNamed("/navigation");
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
BigButtonWidget(
|
||||
iconData: Icons.house,
|
||||
label: "Ingatlanok",
|
||||
onPressed: () async {
|
||||
// Get.toNamed('/map_test');
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
},
|
||||
),
|
||||
BigButtonWidget(
|
||||
iconData: Icons.edit_road,
|
||||
label: "Track",
|
||||
onPressed: () async {
|
||||
// Get.toNamed("/navigation");
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
BigButtonWidget(
|
||||
iconData: Icons.house,
|
||||
label: "Ingatlanok",
|
||||
onPressed: () async {
|
||||
// Get.toNamed('/map_test');
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(
|
||||
() => Text(
|
||||
"Verzió: ${controller.packageInfo.value.version}+${controller.packageInfo.value.buildNumber}",
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
)
|
||||
],
|
||||
BigButtonWidget(
|
||||
iconData: Icons.edit_road,
|
||||
label: "Track",
|
||||
onPressed: () async {
|
||||
Get.toNamed("/tracking");
|
||||
// ScaffoldMessenger.of(context)
|
||||
// .showSnackBar(const SnackBar(
|
||||
// content: Text(
|
||||
// "Fejlesztlés alatt",
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// backgroundColor: Colors.black54,
|
||||
// ));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(
|
||||
() => Text(
|
||||
"Verzió: ${controller.packageInfo.value.version}+${controller.packageInfo.value.buildNumber}",
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
)
|
||||
])));
|
||||
],
|
||||
)
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_geojson/flutter_map_geojson.dart';
|
||||
// import 'package:flutter_map_geojson/flutter_map_geojson.dart';
|
||||
import 'package:flutter_map_polywidget/flutter_map_polywidget.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -19,6 +19,9 @@ import 'package:nmea/nmea.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart'
|
||||
as permission_handler;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:terepi_seged/controls/geoid_grid.dart';
|
||||
import 'package:terepi_seged/eov/convert_coordinate.dart';
|
||||
import 'package:terepi_seged/eov/eov.dart';
|
||||
import 'package:terepi_seged/gnss_sentences/gngga.dart';
|
||||
@@ -26,13 +29,15 @@ import 'package:terepi_seged/gnss_sentences/gngst.dart';
|
||||
import 'package:terepi_seged/gnss_sentences/gnrmc.dart';
|
||||
import 'package:terepi_seged/models/point_to_measure.dart';
|
||||
import 'package:terepi_seged/models/point_with_description_model.dart';
|
||||
import 'package:proj4dart/proj4dart.dart' as proj4;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/views/measured_points_table_dialog.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
|
||||
class MapViewController extends GetxController {
|
||||
// String gpsAddress = "E8:31:CD:14:8B:B2";
|
||||
// String gpsAddress = "98:CD:AC:62:FF:4E";
|
||||
RxString gpsAddress = "98:CD:AC:62:FF:36".obs;
|
||||
String gpsName = "TiGNSS Rover-FF4E";
|
||||
RxString gpsName = "TiGNSS Rover-FF4E".obs;
|
||||
// String gpsName = "TiGNSS Rover-8BB2";
|
||||
static const double maxZoomValue = 25.0;
|
||||
Rx<bool> gpsIsConnected = false.obs;
|
||||
@@ -57,6 +62,8 @@ class MapViewController extends GetxController {
|
||||
DateTime.now().add(const Duration(seconds: -30));
|
||||
|
||||
NumberFormat formatEov = NumberFormat("##0,000.0", "hu-HU");
|
||||
NumberFormat formatEovZ = NumberFormat("###0.0", "hu-HU");
|
||||
NumberFormat formatAltitudeError = NumberFormat("####0.000", "hu-HU");
|
||||
NumberFormat formatEovForFile = NumberFormat("#####0.0", "hu-HU");
|
||||
NumberFormat formatWgs84Sec = NumberFormat('00.000', 'hu-HU');
|
||||
|
||||
@@ -65,12 +72,15 @@ class MapViewController extends GetxController {
|
||||
RxDouble gpsLongitude = 0.0.obs;
|
||||
RxString gpsLongitudeDirection = "".obs;
|
||||
RxDouble gpsAltitude = 0.0.obs;
|
||||
RxDouble gpsGeoidSeparation = 0.0.obs;
|
||||
RxInt gpsQuality = 0.obs;
|
||||
RxDouble gpsLatitudeError = 0.0.obs;
|
||||
RxDouble gpsLongitudeError = 0.0.obs;
|
||||
RxDouble gpsAltitudeError = 0.0.obs;
|
||||
Rx<DateTime> gpsDateTime = DateTime(2000).obs;
|
||||
Rx<Eov> eov = Eov(0, 0).obs;
|
||||
Rx<double?> eovHeight = 0.0.obs;
|
||||
|
||||
RxInt latDegree = 0.obs;
|
||||
RxInt latMin = 0.obs;
|
||||
RxDouble latSec = 0.0.obs;
|
||||
@@ -92,6 +102,10 @@ class MapViewController extends GetxController {
|
||||
RxInt pointsToMeasureSelectedValue = (-1).obs;
|
||||
RxDouble distance = 0.0.obs;
|
||||
|
||||
late GeoidGrid geoidGrid;
|
||||
StreamSubscription<LocationData>? _phoneLocationSub;
|
||||
final _phoneLocation = Location();
|
||||
|
||||
TextEditingController pointIdController = TextEditingController();
|
||||
TextEditingController pointDescriptionController = TextEditingController();
|
||||
TextEditingController gpsHeightController = TextEditingController();
|
||||
@@ -103,8 +117,8 @@ class MapViewController extends GetxController {
|
||||
int pointId = 1;
|
||||
String pointIdPrefix = "";
|
||||
String pointIdPostfix = "";
|
||||
String ntripUserName = "";
|
||||
String ntripPassword = "";
|
||||
RxString ntripUserName = "".obs;
|
||||
RxString ntripPassword = "".obs;
|
||||
|
||||
Rx<bool> pointMeasuringDirectionForward = true.obs;
|
||||
|
||||
@@ -112,10 +126,10 @@ class MapViewController extends GetxController {
|
||||
late Directory? directory;
|
||||
late File dataFile;
|
||||
|
||||
late proj4.Projection eovProj, wgsProj;
|
||||
RxBool mapIsInitialized = false.obs;
|
||||
|
||||
GeoJsonParser parser = GeoJsonParser(defaultMarkerColor: Colors.yellow);
|
||||
// GeoJsonParser parser =
|
||||
// GeoJsonParser(defaultMarkerColor: const Color.fromARGB(255, 85, 34, 49));
|
||||
|
||||
final CollectionReference _measuredPoints =
|
||||
FirebaseFirestore.instance.collection('measuredPoints');
|
||||
@@ -123,27 +137,46 @@ class MapViewController extends GetxController {
|
||||
// late SMIBool gpsTrigger;
|
||||
// late StateMachineController riveGpsIconController;
|
||||
|
||||
late SharedPreferences prefs;
|
||||
Rx<bool> isShowPassword = false.obs;
|
||||
final passwordFieldFocusNode = FocusNode();
|
||||
|
||||
late AuthResponse authResponse;
|
||||
late Session? session;
|
||||
late User? user;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
final bytes = (await rootBundle.load('assets/Grids/etrs2eov_notowgs.gsb'))
|
||||
.buffer
|
||||
.asUint8List();
|
||||
proj4.Projection.nadgrid('Etrs2Eov', bytes);
|
||||
// var def =
|
||||
// '+proj=somerc +lat_0=47.14439372222222 +lon_0=19.04857177777778 +k_0=0.99993 +x_0=650000 +y_0=200000 +ellps=GRS67 +towgs84=52.17,-71.82,-14.9,0,0,0,0 +units=m +nadgrids=@ignorable,Etrs2Eov,null +no_defs';
|
||||
var def =
|
||||
'+proj=somerc +lat_0=47.14439372222222 +lon_0=19.04857177777778 +k_0=0.99993 +x_0=650000 +y_0=200000 +ellps=GRS67 +towgs84=52.17,-71.82,-14.9,0,0,0,0 +units=m +nadgrids=Etrs2Eov +no_defs';
|
||||
|
||||
// Named Projection signature, later find it from anywhere via Projection.get('EPSG:23700')
|
||||
eovProj = proj4.Projection.add('EPSG:23700', def);
|
||||
wgsProj = proj4.Projection.WGS84;
|
||||
//Loading, Success, Error handle with 1 line of code
|
||||
nmeaDecoder
|
||||
..registerTalkerSentence("GGA", (line) => Gngga(raw: line))
|
||||
..registerTalkerSentence("RMC", (line) => Gnrmc(raw: line))
|
||||
..registerTalkerSentence("GST", (line) => Gngst(raw: line));
|
||||
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
|
||||
authResponse = await Supabase.instance.client.auth
|
||||
.signInWithPassword(email: 'test.elek.1@email.hu', password: 'demo');
|
||||
session = authResponse.session;
|
||||
user = authResponse.user;
|
||||
|
||||
Supabase.instance.client
|
||||
.channel('public:TerepiSeged_Receiver')
|
||||
.onPostgresChanges(
|
||||
event: PostgresChangeEvent.update,
|
||||
schema: 'public',
|
||||
table: 'TerepiSeged_Receiver',
|
||||
callback: (playload) {
|
||||
var id = playload.newRecord['pointNumber'] as int;
|
||||
print('Change received: ${id}');
|
||||
updatePointStatus(id);
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx');
|
||||
|
||||
mapController = MapController();
|
||||
|
||||
// riveGpsIconController = RiveUtils.getRiveController(Artboard(),
|
||||
@@ -154,7 +187,7 @@ class MapViewController extends GetxController {
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
void onClose() async {
|
||||
super.onClose();
|
||||
FlutterBluetoothSerial.instance.setPairingRequestHandler(null);
|
||||
if (gpsIsConnected.value) {
|
||||
@@ -162,6 +195,9 @@ class MapViewController extends GetxController {
|
||||
// connection = null;
|
||||
print("BluetoothTestController dispose ....");
|
||||
}
|
||||
|
||||
_stopPhoneGps();
|
||||
|
||||
pointDescriptionController.dispose();
|
||||
pointIdController.dispose();
|
||||
gpsHeightController.dispose();
|
||||
@@ -169,6 +205,10 @@ class MapViewController extends GetxController {
|
||||
pointPostfixController.dispose();
|
||||
ntripUsernameController.dispose();
|
||||
ntripPasswordController.dispose();
|
||||
|
||||
await Supabase.instance.client
|
||||
.channel('public:TerepiSeged_Receiver')
|
||||
.unsubscribe();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -200,6 +240,30 @@ class MapViewController extends GetxController {
|
||||
} else {
|
||||
print("No external storage permission");
|
||||
}
|
||||
if (prefs.containsKey('gpsAddress')) {
|
||||
var address = prefs.getString('gpsAddress');
|
||||
if (address != null) {
|
||||
gpsAddress.value = address;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('gpsName')) {
|
||||
var name = prefs.getString('gpsName');
|
||||
if (name != null) {
|
||||
gpsName.value = name;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('ntripUserName')) {
|
||||
var userName = prefs.getString('ntripUserName');
|
||||
if (userName != null) {
|
||||
ntripUserName.value = userName;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('ntripPassword')) {
|
||||
var password = prefs.getString('ntripPassword');
|
||||
if (password != null) {
|
||||
ntripPassword.value = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
directory = await getExternalStorageDirectory();
|
||||
@@ -228,7 +292,7 @@ class MapViewController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
gpsHeightController.text = '1.7';
|
||||
gpsHeightController.text = '1.8';
|
||||
}
|
||||
|
||||
void _getInitialLocation() async {
|
||||
@@ -260,6 +324,57 @@ class MapViewController extends GetxController {
|
||||
mapController.move(LatLng(currentLatitude.value, currentLongitude.value),
|
||||
currentZoom.value);
|
||||
_updateCurrentLocationMarker();
|
||||
|
||||
if (!gpsIsConnected.value) {
|
||||
await _startPhoneGps();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startPhoneGps() async {
|
||||
if (_phoneLocationSub != null) return;
|
||||
|
||||
// Frissítési beállítások — ezt a location csomag megköveteli
|
||||
await _phoneLocation.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 1000, // ms — másodpercenkénti frissítés
|
||||
distanceFilter: 0, // méter — minden frissítés jöjjön
|
||||
);
|
||||
|
||||
// Engedély teljes körű ellenőrzése
|
||||
var permission = await _phoneLocation.hasPermission();
|
||||
if (permission == PermissionStatus.denied) {
|
||||
permission = await _phoneLocation.requestPermission();
|
||||
}
|
||||
if (permission != PermissionStatus.granted &&
|
||||
permission != PermissionStatus.grantedLimited) {
|
||||
return;
|
||||
}
|
||||
|
||||
// GPS szolgáltatás ellenőrzése
|
||||
bool serviceEnabled = await _phoneLocation.serviceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await _phoneLocation.requestService();
|
||||
if (!serviceEnabled) return;
|
||||
}
|
||||
|
||||
print('Phone GPS: stream starting...');
|
||||
|
||||
_phoneLocationSub = _phoneLocation.onLocationChanged.listen((data) {
|
||||
if (gpsIsConnected.value) {
|
||||
_stopPhoneGps();
|
||||
return;
|
||||
}
|
||||
if (data.latitude == null || data.longitude == null) return;
|
||||
|
||||
currentLatitude.value = data.latitude!;
|
||||
currentLongitude.value = data.longitude!;
|
||||
_updateCurrentLocationMarker();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopPhoneGps() {
|
||||
_phoneLocationSub?.cancel();
|
||||
_phoneLocationSub = null;
|
||||
}
|
||||
|
||||
void connectToGps() {
|
||||
@@ -268,6 +383,7 @@ class MapViewController extends GetxController {
|
||||
connection = value;
|
||||
gpsIsConnected.value = true;
|
||||
print("GPS is connected ...");
|
||||
_stopPhoneGps();
|
||||
|
||||
connection.input!.listen(_onDataReceived);
|
||||
});
|
||||
@@ -278,6 +394,7 @@ class MapViewController extends GetxController {
|
||||
if (gpsIsConnected.value) {
|
||||
connection.close();
|
||||
gpsIsConnected.value = false;
|
||||
_startPhoneGps();
|
||||
print("GPS is disconnected ....");
|
||||
}
|
||||
if (ntripIsConnected.value) {
|
||||
@@ -302,7 +419,9 @@ class MapViewController extends GetxController {
|
||||
header += "User-Agent: SharpGps iter.dk\r\n";
|
||||
header += "Accept: */*\r\nConnection: close\r\n";
|
||||
// header += "Authorization: Basic ${_toBase64("info@mail.app-dev.hu:")}\r\n";
|
||||
header += "Authorization: Basic ${_toBase64("elgi08:Laszl0stef1")}\r\n";
|
||||
//header += "Authorization: Basic ${_toBase64("elgi08:Laszl0stef1")}\r\n";
|
||||
header +=
|
||||
"Authorization: Basic ${_toBase64("${ntripUserName.value}:${ntripPassword.value}")}\r\n";
|
||||
// header += "Host:rtk2go.com:2101\r\n";
|
||||
header += "Host:gnssnet.hu:2101\r\n";
|
||||
header += "Ntrip-Vesrsion:Ntrip/2.0\r\n";
|
||||
@@ -412,17 +531,23 @@ class MapViewController extends GetxController {
|
||||
gpsLongitude.value = sentence.longitude;
|
||||
gpsLongitudeDirection.value = sentence.longitudeDirection;
|
||||
gpsAltitude.value = sentence.altitudeAboveMeanSeaLevel;
|
||||
gpsGeoidSeparation.value = sentence.geoidSeparation;
|
||||
gpsQuality.value = sentence.gpsQualityIndicator;
|
||||
eov.value = ConvertCoordinate.ConvertWgsToEov(
|
||||
gpsLatitude.value, gpsLongitude.value);
|
||||
eovHeight.value = geoidGrid?.toEovHeight(
|
||||
gpsLatitude.value,
|
||||
gpsLongitude.value,
|
||||
gpsAltitude.value,
|
||||
gpsGeoidSeparation.value);
|
||||
|
||||
if (pointsToMeasureSelectedValue.value >= 0) {
|
||||
double coordX =
|
||||
pointsToMeasure[pointsToMeasureSelectedValue.value].coordX;
|
||||
double coordY =
|
||||
pointsToMeasure[pointsToMeasureSelectedValue.value].coordY;
|
||||
var eovCoord = proj4.Point(x: coordX, y: coordY);
|
||||
var wgsCoord = eovProj.transform(wgsProj, eovCoord);
|
||||
var wgsCoord =
|
||||
CoordConverterService.to.eovToWgsPoint(coordX, coordY);
|
||||
distance.value = calculateDistance(
|
||||
LatLng(gpsLatitude.value, gpsLongitude.value),
|
||||
LatLng(wgsCoord.y, wgsCoord.x));
|
||||
@@ -441,7 +566,7 @@ class MapViewController extends GetxController {
|
||||
(DateTime.now()
|
||||
.difference(lastSendTimeGgaMessage)
|
||||
.inSeconds >=
|
||||
30)) {
|
||||
5)) {
|
||||
sendGgaMessage(lastGgaMessage);
|
||||
print("Send GGA message: $lastGgaMessage");
|
||||
ggaSenDataPacketNumber.value++;
|
||||
@@ -723,6 +848,31 @@ class MapViewController extends GetxController {
|
||||
"gpsHeight": gpsHeightController.text
|
||||
});
|
||||
|
||||
await Supabase.instance.client
|
||||
.from('TerepiSeged_MeasuredPoints')
|
||||
.insert({
|
||||
'pointNumber': pointId,
|
||||
'gnssNumber': gpsName.value,
|
||||
'latitude': gpsLatitude.value,
|
||||
'longitude': gpsLongitude.value,
|
||||
'altitude': gpsAltitude.value,
|
||||
'heightOfGeoid': gpsGeoidSeparation.value,
|
||||
'eovX': eov.value.X,
|
||||
'eovY': eov.value.Y,
|
||||
'poleHeight': double.tryParse(gpsHeightController.text),
|
||||
'horizontalError':
|
||||
max(gpsLatitudeError.value, gpsLongitudeError.value),
|
||||
'verticalError': gpsAltitudeError.value,
|
||||
'description': pointDescriptionController.text,
|
||||
'isDeleted': false,
|
||||
'projectId': 2
|
||||
});
|
||||
|
||||
await Supabase.instance.client
|
||||
.from('TerepiSeged_Receiver')
|
||||
.update({'isMeasured': true}).eq(
|
||||
'pointNumber', pointId);
|
||||
|
||||
if (pointMeasuringDirectionForward.isTrue) {
|
||||
if (pointsToMeasureSelectedValue.value <
|
||||
pointsToMeasure.length - 1) {
|
||||
@@ -802,8 +952,8 @@ class MapViewController extends GetxController {
|
||||
pointsToMeasure
|
||||
.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY));
|
||||
|
||||
var eovCoord = proj4.Point(x: coordX, y: coordY);
|
||||
var wgsCoord = eovProj.transform(wgsProj, eovCoord);
|
||||
var wgsCoord =
|
||||
CoordConverterService.to.eovToWgsPoint(coordX, coordY);
|
||||
|
||||
print("Lat -> ${wgsCoord.x}, Long -> ${wgsCoord.y}");
|
||||
|
||||
@@ -853,6 +1003,78 @@ class MapViewController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void readPointsFromSupa() async {
|
||||
// final AuthResponse res = await Supabase.instance.client.auth
|
||||
// .signInWithPassword(email: 'test.elek.1@email.hu', password: 'demo');
|
||||
// final Session? session = res.session;
|
||||
// final User? user = res.user;
|
||||
|
||||
final data =
|
||||
await Supabase.instance.client.from('TerepiSeged_Receiver').select();
|
||||
print('supa count ->${data.length}');
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
var items = data[i];
|
||||
if (items.length >= 3) {
|
||||
var id = items['pointNumber'];
|
||||
num numEovX = items['eovX'];
|
||||
var coordX = numEovX.toDouble();
|
||||
num numEovY = items['eovY'];
|
||||
var coordY = numEovY.toDouble();
|
||||
|
||||
if (id != null && coordX != null && coordY != null) {
|
||||
pointsToMeasure
|
||||
.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY));
|
||||
|
||||
var wgsCoord = CoordConverterService.to.eovToWgsPoint(coordX, coordY);
|
||||
|
||||
print("Lat -> ${wgsCoord.x}, Long -> ${wgsCoord.y}");
|
||||
|
||||
pointsToMeasureMarker.add(Marker(
|
||||
point: LatLng(wgsCoord.y, wgsCoord.x),
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
child: Container(
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(width: 1.0, color: Colors.black)),
|
||||
)));
|
||||
|
||||
pointsToMeasureLabel.add(PolyWidget(
|
||||
center: LatLng(wgsCoord.y + 0.0000075, wgsCoord.x + 0.0000075),
|
||||
widthInMeters: 3,
|
||||
heightInMeters: 3,
|
||||
// constraints: const BoxConstraints(
|
||||
// minWidth: 250,
|
||||
// maxWidth: 350,
|
||||
// ),
|
||||
child: FittedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
' $id ',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.yellow,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
)));
|
||||
|
||||
pointsToMeasureDropDownMenuItem.add(DropdownMenuItem<int>(
|
||||
value: i,
|
||||
child: Text('$id'),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
pointsToMeasureSelectedValue.value = 0;
|
||||
print('Converted points number -> ${pointsToMeasure.length}');
|
||||
}
|
||||
|
||||
void pointsToMeasureSelectedValueChanged(int value) {
|
||||
pointsToMeasureSelectedValue.value = value;
|
||||
print('Selected point -> ${pointsToMeasureSelectedValue.value}');
|
||||
@@ -887,4 +1109,101 @@ class MapViewController extends GetxController {
|
||||
void showAddPointDialog() {
|
||||
onBottomNavigationBarTap(0);
|
||||
}
|
||||
|
||||
void saveGpsAddress(String address) {
|
||||
prefs.setString('gpsAddress', address);
|
||||
}
|
||||
|
||||
void saveGpsName(String name) {
|
||||
prefs.setString('gpsName', name);
|
||||
}
|
||||
|
||||
void saveNtripUserName(String username) {
|
||||
prefs.setString('ntripUserName', username);
|
||||
}
|
||||
|
||||
void saveNtripPassword(String password) {
|
||||
prefs.setString('ntripPassword', password);
|
||||
}
|
||||
|
||||
void updatePointStatus(int pointId) {}
|
||||
|
||||
void toggleShowPassword() {
|
||||
isShowPassword.value = !isShowPassword.value;
|
||||
if (passwordFieldFocusNode.hasPrimaryFocus) return;
|
||||
passwordFieldFocusNode.canRequestFocus = false;
|
||||
}
|
||||
|
||||
void showMeasuredPointsTableDialog() {
|
||||
Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn);
|
||||
}
|
||||
|
||||
Future<List> readMeasuredPoints() async {
|
||||
var response = await Supabase.instance.client
|
||||
.from('TerepiSeged_MeasuredPoints')
|
||||
.select()
|
||||
.eq('projectId', 2)
|
||||
.order('created_at');
|
||||
|
||||
print(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
void SaveMeasuredPointsToFile() async {
|
||||
// var pointsDirectory = await getExternalStorageDirectory();
|
||||
var pointsDirectory = await getApplicationDocumentsDirectory();
|
||||
print(directory!.path);
|
||||
// String newPath = '';
|
||||
// List<String> folders = directory!.path.split("/");
|
||||
// for (int i = 1; i < folders.length; i++) {
|
||||
// String folder = folders[i];
|
||||
// if (folder != "Android") {
|
||||
// newPath += "/" + folder;
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// newPath = newPath + "/TerepisSegedApp";
|
||||
// directory = Directory(newPath);
|
||||
if (!await pointsDirectory!.exists()) {
|
||||
await pointsDirectory.create(recursive: true);
|
||||
}
|
||||
var oldMeasuredPointsFile = File("${directory!.path}/measuredsPoints.csv");
|
||||
|
||||
if (await oldMeasuredPointsFile.exists()) {
|
||||
await oldMeasuredPointsFile.delete();
|
||||
}
|
||||
|
||||
var measuredPointsFile =
|
||||
await File("${directory!.path}/measuredsPoints.csv").create();
|
||||
|
||||
if (await pointsDirectory.exists()) {
|
||||
if (await measuredPointsFile.exists()) {
|
||||
await measuredPointsFile.writeAsString(
|
||||
"Id;DateTime;Description;EovX;EovY;Altitude;Hor.Err;Vert.Err\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
var data = await readMeasuredPoints();
|
||||
|
||||
data.forEach((d) {
|
||||
measuredPointsFile.writeAsStringSync(
|
||||
"${d['id']};${d['created_at']};${d['description']};${formatEov.format(d['eovY'])};${formatEov.format(d['eovX'])};${formatEovZ.format(d['altitude'] - d['poleHeight'])};${formatAltitudeError.format(d['horizontalError'])};${formatAltitudeError.format(d['verticalError'])}\r\n",
|
||||
flush: true,
|
||||
mode: FileMode.append,
|
||||
encoding: utf8);
|
||||
});
|
||||
|
||||
print('Number of data: ${data.length}');
|
||||
|
||||
final params = ShareParams(
|
||||
text: "Mérési eredmények",
|
||||
// files: [XFile('$measuredPointsFile')],
|
||||
files: [XFile("${directory!.path}/measuredsPoints.csv")],
|
||||
subject: 'Mérési eredmények',
|
||||
title: 'Mérési eredmények');
|
||||
|
||||
final result = await SharePlus.instance.share(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class MapView extends GetView<MapViewController> {
|
||||
children: [
|
||||
Text('Térkép'),
|
||||
Text(
|
||||
"Budapest",
|
||||
"",
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
@@ -89,8 +89,27 @@ class MapView extends GetView<MapViewController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.to(() => SettingsDialog(),
|
||||
transition: Transition.downToUp),
|
||||
onTap: () {
|
||||
controller.readPointsFromSupa();
|
||||
},
|
||||
child: const Icon(Icons.cloud_download,
|
||||
size: 26.0, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.ntripUserName.value.isNotEmpty) {
|
||||
controller.ntripUsernameController.text =
|
||||
controller.ntripUserName.value;
|
||||
}
|
||||
if (controller.ntripPassword.value.isNotEmpty) {
|
||||
controller.ntripPasswordController.text =
|
||||
controller.ntripPassword.value;
|
||||
}
|
||||
Get.to(() => SettingsDialog(), transition: Transition.downToUp);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.more_vert,
|
||||
size: 26.0,
|
||||
@@ -135,7 +154,7 @@ class MapView extends GetView<MapViewController> {
|
||||
"EovX: ${controller.formatEov.format(controller.eov.value.Y)}"),
|
||||
Text(
|
||||
"EovY: ${controller.formatEov.format(controller.eov.value.X)}"),
|
||||
Text("Alt: ${controller.gpsAltitude} (m)"),
|
||||
Text("Alt: ${controller.eovHeight.value} (m)"),
|
||||
const Divider(
|
||||
height: 5.0,
|
||||
color: Colors.black,
|
||||
@@ -224,10 +243,11 @@ class MapView extends GetView<MapViewController> {
|
||||
? FlutterMap(
|
||||
mapController: controller.mapController,
|
||||
options: MapOptions(
|
||||
center: LatLng(controller.currentLatitude.value,
|
||||
initialCenter: LatLng(
|
||||
controller.currentLatitude.value,
|
||||
controller.currentLongitude.value),
|
||||
maxZoom: 25,
|
||||
zoom: controller.currentZoom.value,
|
||||
initialZoom: controller.currentZoom.value,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -243,7 +263,7 @@ class MapView extends GetView<MapViewController> {
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: controller.currentLocationMarker),
|
||||
MarkerLayer(markers: controller.parser.markers),
|
||||
// MarkerLayer(markers: controller.parser.markers),
|
||||
MarkerLayer(
|
||||
markers: controller.pointsToMeasureMarker),
|
||||
// PolylineLayer(
|
||||
@@ -438,14 +458,7 @@ class MapView extends GetView<MapViewController> {
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
// controller.isMapMoveToCenter();
|
||||
// controller.addMeasuredPoint();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
"Fejlesztlés alatt",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: Colors.black54,
|
||||
));
|
||||
controller.showMeasuredPointsTableDialog();
|
||||
},
|
||||
heroTag: 'Database test',
|
||||
tooltip: 'Pont bemérése',
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/controllers/map_controller.dart';
|
||||
|
||||
class MeasuredPointsTableDialog extends StatelessWidget {
|
||||
final controller = Get.find<MapViewController>();
|
||||
MeasuredPointsTableDialog({super.key});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.SaveMeasuredPointsToFile();
|
||||
},
|
||||
icon: const Icon(Icons.save)),
|
||||
]),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateProperty.all(Colors.transparent)),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: const Text(
|
||||
'Bezár',
|
||||
style: TextStyle(color: Colors.blue, fontSize: 14.0),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Text(
|
||||
'Bemért pontok',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<dynamic>>(
|
||||
future: controller.readMeasuredPoints(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: Text("No Data available.\n Create new Data"));
|
||||
}
|
||||
// print(snapshot.data);
|
||||
// return const Center(child: Text("Data available."));
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, int index) {
|
||||
var data = snapshot.data![index];
|
||||
print("snapshot data:");
|
||||
print(data);
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(0xff764abc),
|
||||
child: Text((index + 1).toString())),
|
||||
title: Text(data['pointNumber'].toString(),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data['description'],
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700)),
|
||||
Text(
|
||||
"EovX: ${controller.formatEov.format(data['eovY'])} - EovY: ${controller.formatEov.format(data['eovX'])}",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
Text(
|
||||
"EovZ: ${controller.formatEovZ.format(data['altitude'] - data['poleHeight'])} (m)",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
Text(
|
||||
"H.hiba: ${controller.formatAltitudeError.format(data['horizontalError'])} (m) - V.hiba: ${controller.formatAltitudeError.format(data['verticalError'])} (m)",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
],
|
||||
));
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ class SettingsDialog extends StatelessWidget {
|
||||
final controller = Get.find<MapViewController>();
|
||||
SettingsDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
@@ -18,6 +17,19 @@ class SettingsDialog extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
controller.ntripUserName.value =
|
||||
controller.ntripUsernameController.text;
|
||||
controller.saveNtripUserName(
|
||||
controller.ntripUsernameController.text);
|
||||
if (controller
|
||||
.ntripPasswordController.text.isNotEmpty) {
|
||||
controller.ntripPassword.value =
|
||||
controller.ntripPasswordController.text;
|
||||
controller.saveNtripPassword(
|
||||
controller.ntripPasswordController.text);
|
||||
}
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
@@ -25,7 +37,22 @@ class SettingsDialog extends StatelessWidget {
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateProperty.all(Colors.transparent)),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
controller.ntripUserName.value =
|
||||
controller.ntripUsernameController.text;
|
||||
controller.saveNtripUserName(
|
||||
controller.ntripUsernameController.text);
|
||||
if (controller
|
||||
.ntripPasswordController.text.isNotEmpty) {
|
||||
controller.ntripPassword.value =
|
||||
controller.ntripPasswordController.text;
|
||||
controller.saveNtripPassword(
|
||||
controller.ntripPasswordController.text);
|
||||
}
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
child: const Text(
|
||||
'Bezár',
|
||||
style: TextStyle(color: Colors.blue, fontSize: 14.0),
|
||||
@@ -48,12 +75,95 @@ class SettingsDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Obx(() => Column(children: [
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-BE6A'),
|
||||
value: '10:06:1C:97:BE:6A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-BE6A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-BE6A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-1DC6'),
|
||||
value: 'E8:31:CD:16:1D:C6',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-1DC6';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-1DC6');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-9C3A'),
|
||||
value: '08:3A:8D:14:9C:3A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-9C3A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-9C3A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-72C2'),
|
||||
value: '10:06:1C:97:72:C2',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-72C2';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-72C2');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FE16'),
|
||||
value: '10:06:1C:9F:FE:16',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FE16';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FE16');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-3B0A'),
|
||||
value: '10:C6:1C:9E:3B:0A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-3B0A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-3B0A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-7FEA'),
|
||||
value: '10:06:1C:9C:7F:EA',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-7FEA';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-7FEA');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-A39E'),
|
||||
value: '10:06:1C:97:A3:9E',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-A39E';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-A39E');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FF4E'),
|
||||
value: '98:CD:AC:62:FF:4E',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FF4E';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FF4E');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-8BB2'),
|
||||
@@ -61,7 +171,9 @@ class SettingsDialog extends StatelessWidget {
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
;
|
||||
controller.gpsName.value = 'TiGNSS Rover-8BB2';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-8BB2');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FF36'),
|
||||
@@ -69,6 +181,9 @@ class SettingsDialog extends StatelessWidget {
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FF36';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FF36');
|
||||
})
|
||||
])),
|
||||
const Padding(
|
||||
@@ -215,26 +330,54 @@ class SettingsDialog extends StatelessWidget {
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controller.ntripUsernameController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade300,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
labelText: 'Felhasználónév',
|
||||
icon: Icon(Icons.account_circle_rounded)),
|
||||
prefixIcon: Icon(Icons.account_circle_rounded)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: controller.ntripPasswordController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Jelszó',
|
||||
icon: Icon(
|
||||
Icons.lock,
|
||||
)),
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: !controller.isShowPassword.value,
|
||||
focusNode: controller.passwordFieldFocusNode,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: controller.ntripPasswordController,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade300,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
labelText: 'Jelszó',
|
||||
prefixIcon: Icon(
|
||||
Icons.lock_rounded,
|
||||
size: 24,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
child: GestureDetector(
|
||||
onTap: controller.toggleShowPassword,
|
||||
child: Icon(
|
||||
controller.isShowPassword.value
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
size: 24)))),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
|
||||
class MapSurveyBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => MapSurveyController());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
|
||||
class MapAddPointDialog extends GetView<MapSurveyController> {
|
||||
const MapAddPointDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.pointIdController,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Azonosító'),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20.0,
|
||||
),
|
||||
TextField(
|
||||
controller: controller.pointDescriptionController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Leírás'),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(120.0, 40.0)),
|
||||
child: const Text(
|
||||
"Mégsem",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(120.0, 40.0)),
|
||||
child: const Text(
|
||||
"Ment",
|
||||
style: TextStyle(
|
||||
color: Colors.green, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: () async {
|
||||
// pointId = int.parse(pointIdController.text);
|
||||
// pointWithDescriptionList.add(PointWithDescription(
|
||||
// pointId,
|
||||
// gpsDateTime.value,
|
||||
// pointDescriptionController.text,
|
||||
// eov.value.Y,
|
||||
// eov.value.X,
|
||||
// gpsLatitude.value,
|
||||
// gpsLongitude.value,
|
||||
// max(gpsLatitudeError.value, gpsLongitudeError.value),
|
||||
// gpsAltitudeError.value));
|
||||
// print(
|
||||
// "pointWithDescriptionList -> ${pointWithDescriptionList.length}");
|
||||
// pointNotesMarker.add(Marker(
|
||||
// point: LatLng(gpsLatitude.value, gpsLongitude.value),
|
||||
// width: 15.0,
|
||||
// height: 15.0,
|
||||
// builder: (ctx) => Container(
|
||||
// width: 15.0,
|
||||
// height: 15.0,
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.purple,
|
||||
// shape: BoxShape.circle,
|
||||
// border: Border.all(
|
||||
// width: 1.0, color: Colors.black)),
|
||||
// )));
|
||||
// await dataFile.writeAsString(
|
||||
// "$pointId;$gpsDateTime;${pointDescriptionController.text};${formatEovForFile.format(eov.value.Y)};${formatEovForFile.format(eov.value.X)};$gpsLatitude;$gpsLongitude;$gpsAltitude;${max(gpsLatitudeError.value, gpsLongitudeError.value)};$gpsAltitudeError\r\n",
|
||||
// mode: FileMode.append);
|
||||
|
||||
// pointId++;
|
||||
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor.dart';
|
||||
import 'package:flutter_map_polywidget/flutter_map_polywidget.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/enums/map_edit_tool.dart';
|
||||
import 'package:terepi_seged/enums/map_measure_type.dart';
|
||||
import 'package:terepi_seged/enums/map_survey_mode.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_map_layer.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_panel.dart';
|
||||
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/utils/rive_utils.dart';
|
||||
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
||||
import 'package:terepi_seged/widgets/map/distance_or_area_measure_layer.dart';
|
||||
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
|
||||
import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart';
|
||||
import 'package:terepi_seged/widgets/map/note_item_label_layer.dart';
|
||||
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
||||
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart';
|
||||
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart';
|
||||
import 'package:terepi_seged/widgets/map_info_card_column.dart';
|
||||
import 'package:terepi_seged/widgets/save_point_fab.dart';
|
||||
import 'package:terepi_seged/widgets/shared_map_widgets.dart';
|
||||
|
||||
import 'map_add_point_dialog.dart';
|
||||
|
||||
class MapSurveyView extends GetView<MapSurveyController> {
|
||||
const MapSurveyView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
SharedMapWidget(
|
||||
controls: const MapControls(),
|
||||
mapController: controller.mapController,
|
||||
isFollowing: controller.isMapMoveToCenter,
|
||||
initialCenter: LatLng(
|
||||
// ← nem fix érték
|
||||
controller.currentLatitude.value != 0.0
|
||||
? controller.currentLatitude.value
|
||||
: 47.5,
|
||||
controller.currentLongitude.value != 0.0
|
||||
? controller.currentLongitude.value
|
||||
: 19.0,
|
||||
),
|
||||
initialZoom: controller.currentZoom.value,
|
||||
currentZoom: controller.currentZoom,
|
||||
onZoomIn: controller.mapZoomIn,
|
||||
onZoomOut: controller.mapZoomOut,
|
||||
onPositionChanged: controller.onMapPositionChanged,
|
||||
onCenterOnGps: controller.setIsMapMoveToCenter,
|
||||
onLongPress: (tapPosition, point) {
|
||||
if (controller.activeEditTool.value == MapEditTool.point) {
|
||||
if (controller.isGeometryEditing) {
|
||||
controller.saveEditedPoint(point: point);
|
||||
} else {
|
||||
//controller.saveEditedPoint(point: point);
|
||||
controller.showSavePointDialog(point: point);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (controller.activeEditTool.value == MapEditTool.line ||
|
||||
controller.activeEditTool.value == MapEditTool.polygon) {
|
||||
controller.polygonEditorController.addPoint(point);
|
||||
}
|
||||
},
|
||||
onTap: (tapPosition, point) {
|
||||
if (controller.isDistanceOrAreaMeasuring) {
|
||||
controller.addDistanceOrAreaMeasuredPoint(point);
|
||||
return;
|
||||
}
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) return;
|
||||
if (controller.isMapEditing) return;
|
||||
final polygonHit = controller.polygonHitNotifier.value;
|
||||
if (polygonHit != null && polygonHit.hitValues.isNotEmpty) {
|
||||
final id = polygonHit.hitValues.first;
|
||||
controller.selectedNoteItem(id);
|
||||
return;
|
||||
}
|
||||
final polylineHit = controller.polylineHitNotifier.value;
|
||||
if (polylineHit != null && polylineHit.hitValues.isNotEmpty) {
|
||||
final id = polylineHit.hitValues.first;
|
||||
controller.selectedNoteItem(id);
|
||||
return;
|
||||
}
|
||||
controller.clearNoteItemSelection();
|
||||
},
|
||||
layers: [
|
||||
const ImportedLayerOverlay(),
|
||||
// Track polyline
|
||||
Obx(() {
|
||||
final inTrackMode = controller.mode.value == MapSurveyMode.track ||
|
||||
controller.mode.value == MapSurveyMode.fieldWalk;
|
||||
if (!inTrackMode) return const SizedBox.shrink();
|
||||
final ids = TrackingController.to.overlayTrackIds;
|
||||
if (ids.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final polylines = ids
|
||||
.map((id) {
|
||||
final pts = TrackingController.to.getCoordsFor(id);
|
||||
if (pts.isEmpty) return null;
|
||||
return Polyline(
|
||||
points: pts,
|
||||
color: Colors.blue.withOpacity(0.75),
|
||||
strokeWidth: 2.5,
|
||||
borderColor: Colors.white.withOpacity(0.4),
|
||||
borderStrokeWidth: 1.0,
|
||||
);
|
||||
})
|
||||
.whereType<Polyline>()
|
||||
.toList();
|
||||
|
||||
if (polylines.isEmpty) return const SizedBox.shrink();
|
||||
return PolylineLayer(polylines: polylines);
|
||||
}),
|
||||
Obx(() {
|
||||
final isTracking = TrackingController.to.isRecording.value;
|
||||
final inTrackMode = controller.mode.value == MapSurveyMode.track;
|
||||
if (!isTracking && !inTrackMode) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return _buildTrackLayer1();
|
||||
}
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.measure) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return MarkerLayer(markers: controller.pointNotesMarker.toList());
|
||||
}),
|
||||
Obx(() {
|
||||
// Pontok - terepi bejárás
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!controller.showPoints.value) return const SizedBox.shrink();
|
||||
|
||||
return MarkerLayer(markers: [...controller.pointNotes]);
|
||||
}),
|
||||
Obx(() {
|
||||
// Vonalak - terepbejárás
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!controller.showLines.value) return const SizedBox.shrink();
|
||||
|
||||
return PolylineLayer(
|
||||
hitNotifier: controller.polylineHitNotifier,
|
||||
polylines: [...controller.polylineNotes]);
|
||||
}),
|
||||
Obx(() {
|
||||
// Poligonok - terepbejárás
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!controller.showPolygons.value) return const SizedBox.shrink();
|
||||
|
||||
return PolygonLayer(
|
||||
hitNotifier: controller.polygonHitNotifier,
|
||||
polygons: [...controller.polygonNotes],
|
||||
useAltRendering: true);
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final selectedId = controller.selectedNoteItemId.value;
|
||||
if (selectedId == null) return const SizedBox.shrink();
|
||||
|
||||
// Polygon kiemelés
|
||||
final selectedPolygon = controller.polygonNotes
|
||||
.where((p) => p.hitValue == selectedId)
|
||||
.firstOrNull;
|
||||
if (selectedPolygon != null) {
|
||||
return PolygonLayer(polygons: [
|
||||
Polygon(
|
||||
points: selectedPolygon.points,
|
||||
color: Colors.transparent,
|
||||
borderColor: Colors.white,
|
||||
borderStrokeWidth: selectedPolygon.borderStrokeWidth + 3,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Polyline kiemelés
|
||||
final selectedPolyline = controller.polylineNotes
|
||||
.where((p) => p.hitValue == selectedId)
|
||||
.firstOrNull;
|
||||
if (selectedPolyline != null) {
|
||||
return PolylineLayer(polylines: [
|
||||
Polyline(
|
||||
points: selectedPolyline.points,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
strokeWidth: selectedPolyline.strokeWidth + 4,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return PolygonEditor(
|
||||
controller: controller.polygonEditorController,
|
||||
throttleDuration: Duration.zero);
|
||||
}),
|
||||
NoteItemLabelLayer(
|
||||
controller: controller,
|
||||
),
|
||||
Obx(() {
|
||||
final tracks = controller.teamTrackPoints;
|
||||
if (tracks.isEmpty) return const SizedBox.shrink();
|
||||
return PolylineLayer(
|
||||
polylines: tracks.entries.map((e) {
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.purple,
|
||||
Colors.teal,
|
||||
Colors.indigo,
|
||||
Colors.cyan,
|
||||
Colors.deepPurple
|
||||
];
|
||||
final color = colors[e.key.hashCode.abs() % colors.length];
|
||||
return Polyline(
|
||||
points: e.value,
|
||||
color: color.withValues(alpha: 0.7),
|
||||
strokeWidth: 2.5);
|
||||
}).toList(),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final markers =
|
||||
List<Marker>.from(controller.teamTrackMarkers.values);
|
||||
if (markers.isEmpty) return const SizedBox.shrink();
|
||||
return MarkerLayer(markers: markers);
|
||||
}),
|
||||
DistanceOrAreaMeasureLayer(controller: controller),
|
||||
const StakeoutMapLayers(),
|
||||
Obx(() {
|
||||
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
||||
GnssConnectionType.none;
|
||||
if (isGpsActive && controller.mode.value != MapSurveyMode.track) {
|
||||
return MarkerLayer(
|
||||
markers: controller.currentLocationMarker.toList());
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: MapInfoCardColumn(controller: controller),
|
||||
),
|
||||
|
||||
// Positioned(
|
||||
// top: 390,
|
||||
// right: 60,
|
||||
// left: 8,
|
||||
// child: CoordinatePanel(
|
||||
// eovY: controller.eovY,
|
||||
// eovX: controller.eovX,
|
||||
// horError: controller.gpsLatitudeError,
|
||||
// vertError: controller.gpsAltitudeError,
|
||||
// altitudeMsl: controller.gpsAltitude,
|
||||
// geoidSeparation: controller.gpsGeoidSeparation,
|
||||
// ntripConnected: controller.ntripIsConnected,
|
||||
// ntripBytes: controller.ntripReceivedData,
|
||||
// ntripPackets: controller.ntripDataPacketNumbers,
|
||||
// ggaPackets: controller.ggaSenDataPacketNumber,
|
||||
// ),
|
||||
// ),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (controller.activeEditTool.value == MapEditTool.none) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: MapEditCompactToolbar(controller: controller));
|
||||
}
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: MapEditDrawingToolbar(controller: controller));
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.measure) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: MeasureBottomPanel(ctrl: controller));
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.fieldWalk)
|
||||
return SizedBox.shrink();
|
||||
return Positioned(
|
||||
right: 10,
|
||||
bottom: 300,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// Távolság mérés
|
||||
_MeasureButton(
|
||||
icon: Icons.straighten,
|
||||
tooltip: 'Távolságmérés',
|
||||
active:
|
||||
controller.mapMeasureType.value == MapMeasureType.distance,
|
||||
onTap: controller.toggleMeasureDistance,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Területmérés
|
||||
_MeasureButton(
|
||||
icon: Icons.pentagon_outlined,
|
||||
tooltip: 'Területmérés',
|
||||
active: controller.mapMeasureType.value == MapMeasureType.area,
|
||||
onTap: controller.toggleMeasureArea,
|
||||
),
|
||||
// Visszavonás (ha van pont)
|
||||
if (controller.distanceOrAreaMeasurePoints.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
_MeasureButton(
|
||||
icon: Icons.undo,
|
||||
tooltip: 'Utolsó pont törlése',
|
||||
active: false,
|
||||
onTap: controller.removeLastDistanceOrAreaMeasuredPoint,
|
||||
),
|
||||
]
|
||||
]));
|
||||
}),
|
||||
Obx(() {
|
||||
if (controller.mode.value != MapSurveyMode.stakeout) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return const Positioned(
|
||||
left: 0, right: 0, bottom: 0, child: StakeoutPanel());
|
||||
}),
|
||||
// Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()),
|
||||
// Positioned(
|
||||
// bottom: 80,
|
||||
// left: 8,
|
||||
// right: 8,
|
||||
// child: Obx(
|
||||
// () => controller.mode.value == MapSurveyMode.stakeout
|
||||
// ? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög
|
||||
// : const SizedBox.shrink(),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// bottom: 16,
|
||||
// right: 16,
|
||||
// child: SavePointFab(controller: controller),
|
||||
// ),
|
||||
// Positioned(
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: MapBottomPanel(controller: controller))
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTrackLayer() {
|
||||
// FutureBuilder helyett a controller livePoints-ból
|
||||
return Obx(() {
|
||||
final ctrl = TrackingController.to;
|
||||
if (ctrl.livePoints.isEmpty) return const SizedBox.shrink();
|
||||
return PolylineLayer(polylines: [
|
||||
Polyline(
|
||||
points: ctrl.livePoints
|
||||
.map((p) => LatLng(p.latitude, p.longitude))
|
||||
.toList(),
|
||||
color: Colors.red.withOpacity(0.85),
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTrackLayer1() {
|
||||
return Obx(() {
|
||||
final ctrl = TrackingController.to;
|
||||
final points = ctrl.livePoints.toList();
|
||||
if (points.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Stack(children: [
|
||||
// 1. Track vonal
|
||||
PolylineLayer(polylines: [
|
||||
Polyline(
|
||||
points: points,
|
||||
color: Colors.red.withOpacity(0.85),
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
]),
|
||||
// 2. Markerek a vonal felett
|
||||
MarkerLayer(markers: [
|
||||
// Kezdőpont — zöld
|
||||
Marker(
|
||||
point: points.first,
|
||||
child: const Icon(Icons.flag, color: Colors.green, size: 28),
|
||||
),
|
||||
// Utolsó pont — piros (ha van legalább 2 pont)
|
||||
if (points.length > 1)
|
||||
Marker(
|
||||
point: points.last,
|
||||
child: _PulsingDot(
|
||||
color: TrackingController.to.isPaused.value
|
||||
? Colors.orange
|
||||
: Colors.blue,
|
||||
),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeSelector extends GetView<MapSurveyController> {
|
||||
const _ModeSelector();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => SegmentedButton<MapSurveyMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: MapSurveyMode.measure,
|
||||
icon: Icon(Icons.gps_fixed, size: 16),
|
||||
label: Text('Bemérés'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: MapSurveyMode.stakeout,
|
||||
icon: Icon(Icons.my_location, size: 16),
|
||||
label: Text('Kitűzés'),
|
||||
),
|
||||
],
|
||||
selected: {controller.mode.value},
|
||||
onSelectionChanged: (s) => controller.switchMode(s.first),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _PulsingDot extends StatefulWidget {
|
||||
final Color color;
|
||||
const _PulsingDot({required this.color});
|
||||
|
||||
@override
|
||||
State<_PulsingDot> createState() => _PulsingDotState();
|
||||
}
|
||||
|
||||
class _PulsingDotState extends State<_PulsingDot>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _anim;
|
||||
late Animation<double> _scale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_anim = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 1000))
|
||||
..repeat(reverse: true);
|
||||
_scale = Tween(begin: 0.8, end: 1.1)
|
||||
.animate(CurvedAnimation(parent: _anim, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_anim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaleTransition(
|
||||
scale: _scale,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: widget.color.withOpacity(0.5),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 2)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StakeoutPanel extends GetView<MapSurveyController> {
|
||||
const _StakeoutPanel();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Obx(() {
|
||||
final onTarget = controller.isOnTarget;
|
||||
final dy = controller.deltaY;
|
||||
final dx = controller.deltaX;
|
||||
final dist = controller.distanceToTarget;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Célpont neve
|
||||
Row(children: [
|
||||
const Icon(Icons.flag, size: 16, color: Colors.orange),
|
||||
const SizedBox(width: 6),
|
||||
Text(controller.targetName.value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: _showTargetPicker,
|
||||
child: const Text('Változtat'),
|
||||
),
|
||||
]),
|
||||
|
||||
const Divider(height: 16),
|
||||
|
||||
// Eltérések
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_DeltaCell(
|
||||
label: 'ΔY',
|
||||
value: dy,
|
||||
unit: 'm',
|
||||
),
|
||||
_DeltaCell(
|
||||
label: 'ΔX',
|
||||
value: dx,
|
||||
unit: 'm',
|
||||
),
|
||||
_DeltaCell(
|
||||
label: 'Táv',
|
||||
value: dist,
|
||||
unit: 'm',
|
||||
alwaysPositive: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Státusz
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: onTarget
|
||||
? Colors.green.withOpacity(0.12)
|
||||
: Colors.orange.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
onTarget
|
||||
? '✓ Célponton — pont rögzíthető'
|
||||
: '${controller.distanceToTarget.toStringAsFixed(3)} m a céltól',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: onTarget ? Colors.green : Colors.orange,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTargetPicker() {
|
||||
// Lista a korábban bemért vagy tervezett pontokból
|
||||
//Get.bottomSheet(const _TargetPickerSheet());
|
||||
}
|
||||
}
|
||||
|
||||
class _DeltaCell extends StatelessWidget {
|
||||
final String label;
|
||||
final double value;
|
||||
final String unit;
|
||||
final bool alwaysPositive;
|
||||
|
||||
const _DeltaCell({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
this.alwaysPositive = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final display = alwaysPositive ? value.abs() : value;
|
||||
final prefix = (!alwaysPositive && value > 0) ? '+' : '';
|
||||
final color = value.abs() < 0.05
|
||||
? Colors.green
|
||||
: value.abs() < 0.5
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$prefix${display.toStringAsFixed(3)} $unit',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MeasureButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String tooltip;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
const _MeasureButton({
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.active,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Material(
|
||||
color: active
|
||||
? Colors.deepOrange
|
||||
: isDark
|
||||
? Colors.grey.shade800.withOpacity(0.9)
|
||||
: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
elevation: 3,
|
||||
shadowColor: Colors.black38,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: Center(
|
||||
child: Icon(icon,
|
||||
size: 20,
|
||||
color: active
|
||||
? Colors.white
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
// Bemért pontok listája:
|
||||
// - Pont neve + EOV Y/X
|
||||
// - Törlés swipe-pal
|
||||
// - CSV export (share_plus)
|
||||
// - SQLite alapú (nem memória)
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../controllers/map_survey_controller.dart';
|
||||
import '../../../../../../models/measured_point.dart';
|
||||
import '../../../../../../services/app_database.dart';
|
||||
import '../../../../../../services/project_service.dart';
|
||||
|
||||
class MeasuredPointsSheet extends StatefulWidget {
|
||||
const MeasuredPointsSheet({super.key});
|
||||
|
||||
@override
|
||||
State<MeasuredPointsSheet> createState() => _MeasuredPointsSheetState();
|
||||
}
|
||||
|
||||
class _MeasuredPointsSheetState extends State<MeasuredPointsSheet> {
|
||||
List<MeasuredPoint> _points = [];
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
if (projectId == null) {
|
||||
setState(() => _loading = false);
|
||||
return;
|
||||
}
|
||||
final pts = await AppDatabase.instance.listMeasuredPoints(projectId);
|
||||
if (mounted)
|
||||
setState(() {
|
||||
_points = pts;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Handle ───────────────────────────────────────────────
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Fejléc ───────────────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 12, 8),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.location_on, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text('Bemért pontok',
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 6),
|
||||
if (!_loading)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('${_points.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onPrimaryContainer)),
|
||||
),
|
||||
const Spacer(),
|
||||
// CSV export
|
||||
if (_points.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download_outlined),
|
||||
tooltip: 'CSV export',
|
||||
onPressed: _exportCsv,
|
||||
),
|
||||
// Összes törlése
|
||||
if (_points.isNotEmpty)
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_sweep_outlined,
|
||||
color: Colors.red.shade300),
|
||||
tooltip: 'Összes törlése',
|
||||
onPressed: _deleteAll,
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// ── Lista ─────────────────────────────────────────────────
|
||||
Expanded(
|
||||
child: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _points.isEmpty
|
||||
? _emptyState()
|
||||
: Column(children: [
|
||||
// Fejléc sor
|
||||
_TableHeader(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: _points.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider(height: 1),
|
||||
itemBuilder: (_, i) => _PointTile(
|
||||
point: _points[i],
|
||||
onDelete: () => _deleteOne(_points[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
// Alsó padding
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Törlés ────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _deleteOne(MeasuredPoint pt) async {
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Pont törlése'),
|
||||
content: Text('"${pt.name}" törlése visszavonohatalan.'),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Törlés'),
|
||||
),
|
||||
],
|
||||
));
|
||||
if (ok != true) return;
|
||||
await AppDatabase.instance.deleteMeasuredPoint(pt.id!);
|
||||
setState(() => _points.remove(pt));
|
||||
|
||||
// Controller marker listából is eltávolítjuk
|
||||
final ctrl = Get.find<MapSurveyController>();
|
||||
|
||||
if (pt.latitude != null && pt.longitude != null) {
|
||||
ctrl.pointNotesMarker.removeWhere((m) =>
|
||||
(m.point.latitude - pt.latitude!).abs() < 1e-9 &&
|
||||
(m.point.longitude - pt.longitude!).abs() < 1e-9);
|
||||
}
|
||||
|
||||
ctrl.pointWithDescriptionList
|
||||
.removeWhere((p) => p.pointId.toString() == pt.name);
|
||||
}
|
||||
|
||||
Future<void> _deleteAll() async {
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Összes pont törlése'),
|
||||
content: Text('${_points.length} pont törlése visszavonohatalan.'),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Összes törlése'),
|
||||
),
|
||||
],
|
||||
));
|
||||
if (ok != true) return;
|
||||
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
if (projectId != null) {
|
||||
await AppDatabase.instance.deleteAllMeasuredPoints(projectId);
|
||||
}
|
||||
final ctrl = Get.find<MapSurveyController>();
|
||||
ctrl.pointWithDescriptionList.clear();
|
||||
ctrl.pointNotesMarker.clear();
|
||||
|
||||
setState(() => _points.clear());
|
||||
}
|
||||
|
||||
// ── CSV export ────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _exportCsv() async {
|
||||
final buf = StringBuffer();
|
||||
buf.writeln(MeasuredPoint.csvHeader());
|
||||
for (final pt in _points) {
|
||||
buf.writeln(pt.toCsvRow());
|
||||
}
|
||||
|
||||
final projectName =
|
||||
ProjectService.to.activeProject.value?.name ?? 'projekt';
|
||||
final ts =
|
||||
DateTime.now().toIso8601String().replaceAll(':', '-').substring(0, 16);
|
||||
final name = '${projectName}_bemert_pontok_$ts.csv';
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/$name');
|
||||
await file.writeAsString(buf.toString(), encoding: utf8);
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path, mimeType: 'text/csv; charset=utf-8')],
|
||||
subject: 'Bemért pontok — $projectName',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyState() => Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.location_off_outlined,
|
||||
size: 56, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 12),
|
||||
Text('Még nincs bemért pont',
|
||||
style: TextStyle(color: Colors.grey.shade500)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Fejléc sor ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _TableHeader extends StatelessWidget {
|
||||
const _TableHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final style = TextStyle(
|
||||
fontSize: 11, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant);
|
||||
return Container(
|
||||
color: cs.surfaceContainerHighest,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Row(children: [
|
||||
SizedBox(width: 52, child: Text('Pont', style: style)),
|
||||
Expanded(child: Text('EOV Y', style: style)),
|
||||
Expanded(child: Text('EOV X', style: style)),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text('Leírás', style: style, textAlign: TextAlign.center)),
|
||||
const SizedBox(width: 40), // törlés gomb helye
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Egy pont sor ────────────────────────────────────────────────────────────
|
||||
|
||||
class _PointTile extends StatelessWidget {
|
||||
final MeasuredPoint point;
|
||||
final VoidCallback onDelete;
|
||||
const _PointTile({required this.point, required this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final hasEov = point.eovY != null && point.eovX != null;
|
||||
final accColor = _accColor(point.accuracy);
|
||||
|
||||
return Dismissible(
|
||||
key: ValueKey(point.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (_) async {
|
||||
onDelete();
|
||||
return false; // mi kezeljük a törlést
|
||||
},
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(children: [
|
||||
// Pont neve
|
||||
SizedBox(
|
||||
width: 52,
|
||||
child: Text(point.name,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
// EOV Y
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasEov ? point.eovY!.toStringAsFixed(1) : '–',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]),
|
||||
),
|
||||
),
|
||||
// EOV X
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasEov ? point.eovX!.toStringAsFixed(1) : '–',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]),
|
||||
),
|
||||
),
|
||||
// Pontosság
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(
|
||||
point.note.isEmpty ? '---' : point.note,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: point.note.isEmpty ? Colors.grey.shade400 : null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// Törlés gomb
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete_outline,
|
||||
size: 18, color: Colors.grey.shade400),
|
||||
onPressed: onDelete,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _accColor(double? acc) {
|
||||
if (acc == null) return Colors.grey;
|
||||
if (acc <= 0.05) return Colors.green;
|
||||
if (acc <= 0.20) return Colors.lightGreen.shade700;
|
||||
if (acc <= 1.00) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
|
||||
class MeasuredPointsTableDialog extends StatelessWidget {
|
||||
final controller = Get.find<MapSurveyController>();
|
||||
MeasuredPointsTableDialog({super.key});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.SaveMeasuredPointsToFile();
|
||||
},
|
||||
icon: const Icon(Icons.save)),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateProperty.all(Colors.transparent)),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: const Text(
|
||||
'Bezár',
|
||||
style: TextStyle(color: Colors.blue, fontSize: 14.0),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Text(
|
||||
'Bemért pontok',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<dynamic>>(
|
||||
future: controller.readMeasuredPoints(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: Text("No Data available.\n Create new Data"));
|
||||
}
|
||||
// print(snapshot.data);
|
||||
// return const Center(child: Text("Data available."));
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, int index) {
|
||||
var data = snapshot.data![index];
|
||||
print("snapshot data:");
|
||||
print(data);
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(0xff764abc),
|
||||
child: Text((index + 1).toString())),
|
||||
title: Text(data['pointNumber'].toString(),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data['description'],
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700)),
|
||||
Text(
|
||||
"EovX: ${controller.formatEov.format(data['eovY'])} - EovY: ${controller.formatEov.format(data['eovX'])}",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
Text(
|
||||
"EovZ: ${controller.formatEovZ.format(data['altitude'] - data['poleHeight'])} (m)",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
Text(
|
||||
"H.hiba: ${controller.formatAltitudeError.format(data['horizontalError'])} (m) - V.hiba: ${controller.formatAltitudeError.format(data['verticalError'])} (m)",
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade400)),
|
||||
],
|
||||
));
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
|
||||
class SettingsDialog extends StatelessWidget {
|
||||
final controller = Get.find<MapSurveyController>();
|
||||
SettingsDialog({super.key});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
// controller.ntripUserName.value =
|
||||
// controller.ntripUsernameController.text;
|
||||
// controller.saveNtripUserName(
|
||||
// controller.ntripUsernameController.text);
|
||||
// if (controller
|
||||
// .ntripPasswordController.text.isNotEmpty) {
|
||||
// controller.ntripPassword.value =
|
||||
// controller.ntripPasswordController.text;
|
||||
// controller.saveNtripPassword(
|
||||
// controller.ntripPasswordController.text);
|
||||
// }
|
||||
// }
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateProperty.all(Colors.transparent)),
|
||||
onPressed: () {
|
||||
// if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
// controller.ntripUserName.value =
|
||||
// controller.ntripUsernameController.text;
|
||||
// controller.saveNtripUserName(
|
||||
// controller.ntripUsernameController.text);
|
||||
// if (controller
|
||||
// .ntripPasswordController.text.isNotEmpty) {
|
||||
// controller.ntripPassword.value =
|
||||
// controller.ntripPasswordController.text;
|
||||
// controller.saveNtripPassword(
|
||||
// controller.ntripPasswordController.text);
|
||||
// }
|
||||
// }
|
||||
Get.back();
|
||||
},
|
||||
child: const Text(
|
||||
'Bezár',
|
||||
style: TextStyle(color: Colors.blue, fontSize: 14.0),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Text(
|
||||
'Beállítások',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Text(
|
||||
'GPS vevő',
|
||||
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
// Obx(() => Column(children: [
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-BE6A'),
|
||||
// value: '10:06:1C:97:BE:6A',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-BE6A';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-BE6A');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-1DC6'),
|
||||
// value: 'E8:31:CD:16:1D:C6',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-1DC6';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-1DC6');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-9C3A'),
|
||||
// value: '08:3A:8D:14:9C:3A',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-9C3A';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-9C3A');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-72C2'),
|
||||
// value: '10:06:1C:97:72:C2',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-72C2';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-72C2');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-FE16'),
|
||||
// value: '10:06:1C:9F:FE:16',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-FE16';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-FE16');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-3B0A'),
|
||||
// value: '10:C6:1C:9E:3B:0A',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-3B0A';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-3B0A');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-7FEA'),
|
||||
// value: '10:06:1C:9C:7F:EA',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-7FEA';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-7FEA');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-A39E'),
|
||||
// value: '10:06:1C:97:A3:9E',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-A39E';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-A39E');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-FF4E'),
|
||||
// value: '98:CD:AC:62:FF:4E',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-FF4E';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-FF4E');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-8BB2'),
|
||||
// value: 'E8:31:CD:14:8B:B2',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-8BB2';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-8BB2');
|
||||
// }),
|
||||
// RadioListTile(
|
||||
// title: Text('TiGNSS Rover-FF36'),
|
||||
// value: '98:CD:AC:62:FF:36',
|
||||
// groupValue: controller.gpsAddress.value,
|
||||
// onChanged: (value) {
|
||||
// controller.gpsAddress.value = value!;
|
||||
// controller.gpsName.value = 'TiGNSS Rover-FF36';
|
||||
// controller.saveGpsAddress(value);
|
||||
// controller.saveGpsName('TiGNSS Rover-FF36');
|
||||
// })
|
||||
// ])),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(
|
||||
height: 5,
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 5),
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
// child: Text(
|
||||
// 'Bemért pont azonosítója:',
|
||||
// style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Flexible(
|
||||
// child: TextField(
|
||||
// controller: controller.pointPrefixController,
|
||||
// decoration: const InputDecoration(
|
||||
// border: OutlineInputBorder(), labelText: 'Előtag'),
|
||||
// ),
|
||||
// ),
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
// child: Text(
|
||||
// 'Eredeti azonosító',
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// ),
|
||||
// ),
|
||||
// Flexible(
|
||||
// child: TextField(
|
||||
// controller: controller.pointPostfixController,
|
||||
// decoration: const InputDecoration(
|
||||
// border: OutlineInputBorder(), labelText: 'Utótag'),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 5),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
'Mérés iránya:',
|
||||
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Column(children: [
|
||||
RadioListTile<bool>(
|
||||
title: Text('Növekvő'),
|
||||
value: true,
|
||||
groupValue: controller.pointMeasuringDirectionForward.value,
|
||||
onChanged: (value) {
|
||||
controller.pointMeasuringDirectionForward.value = value!;
|
||||
}),
|
||||
RadioListTile<bool>(
|
||||
title: Text('Csökkenő'),
|
||||
value: false,
|
||||
groupValue: controller.pointMeasuringDirectionForward.value,
|
||||
onChanged: (value) {
|
||||
controller.pointMeasuringDirectionForward.value = value!;
|
||||
})
|
||||
]),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(
|
||||
height: 5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
'Kitűzött pont azonostója:',
|
||||
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: controller.pointPrefixController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Előtag'),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Text(
|
||||
'Eredeti azonosító',
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: controller.pointPostfixController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Utótag'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 8),
|
||||
child: Text(
|
||||
'Például: előtag: 6, utótag: -bp, eredeti azonositó: 1002 -> 61002-bp',
|
||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.w400))),
|
||||
const SizedBox(height: 15),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(
|
||||
height: 5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
'Ntrip szolgáltatás:',
|
||||
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
//controller: NtripService.to.usernameController,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade300,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
labelText: 'Felhasználónév',
|
||||
prefixIcon: Icon(Icons.account_circle_rounded)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: controller.isShowPassword.value,
|
||||
//focusNode: controller.passwordFieldFocusNode,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
//controller: NtripService.to.passwordController,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade300,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
labelText: 'Jelszó',
|
||||
prefixIcon: Icon(
|
||||
Icons.lock_rounded,
|
||||
size: 24,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
child: GestureDetector(
|
||||
//onTap: controller.toggleShowPassword,
|
||||
child: Icon(
|
||||
controller.isShowPassword.value
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
size: 24)))),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20)
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_export_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_service.dart';
|
||||
|
||||
import '../../../../models/stakeout_point.dart';
|
||||
|
||||
/// Kitűzési pontlista — bottom sheet a panelről.
|
||||
///
|
||||
/// Szűrés státusz és vonal szerint; soronként távolság a jelenlegi
|
||||
/// pozíciótól, kitűzötteknél az eltérések; koppintás = az legyen a cél;
|
||||
/// menü: kihagyás / visszaállítás / törlés. Fejlécből CSV-export
|
||||
/// (magyar Excel-barát vagy nemzetközi dialektus).
|
||||
class StakeoutListSheet extends StatefulWidget {
|
||||
const StakeoutListSheet({super.key});
|
||||
|
||||
static Future<void> show() {
|
||||
return Get.bottomSheet(
|
||||
const StakeoutListSheet(),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<StakeoutListSheet> createState() => _StakeoutListSheetState();
|
||||
}
|
||||
|
||||
class _StakeoutListSheetState extends State<StakeoutListSheet> {
|
||||
StakeoutStatus? _statusFilter; // null = mind
|
||||
String? _lineFilter; // null = mind
|
||||
|
||||
StakeoutService get svc => StakeoutService.to;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Material(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.85),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Fejléc + export ────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.list_alt),
|
||||
const SizedBox(width: 8),
|
||||
Obx(() => Text(
|
||||
'Kitűzési pontok (${svc.points.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.ios_share),
|
||||
tooltip: 'Export CSV',
|
||||
onPressed: _exportDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Szűrők ─────────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SegmentedButton<StakeoutStatus?>(
|
||||
segments: const [
|
||||
ButtonSegment(value: null, label: Text('Mind')),
|
||||
ButtonSegment(
|
||||
value: StakeoutStatus.pending,
|
||||
label: Text('Vár')),
|
||||
ButtonSegment(
|
||||
value: StakeoutStatus.staked,
|
||||
label: Text('Kész')),
|
||||
ButtonSegment(
|
||||
value: StakeoutStatus.skipped,
|
||||
label: Text('Kihagyva')),
|
||||
],
|
||||
selected: {_statusFilter},
|
||||
onSelectionChanged: (s) =>
|
||||
setState(() => _statusFilter = s.first),
|
||||
showSelectedIcon: false,
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Obx(() {
|
||||
final lines = svc.lines;
|
||||
if (lines.length <= 1) return const SizedBox.shrink();
|
||||
return DropdownButton<String?>(
|
||||
value: _lineFilter,
|
||||
hint:
|
||||
const Text('Vonal', style: TextStyle(fontSize: 12)),
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: null, child: Text('Mind')),
|
||||
for (final l in lines)
|
||||
DropdownMenuItem(
|
||||
value: l,
|
||||
child: Text(l.isEmpty ? '(nincs)' : l)),
|
||||
],
|
||||
onChanged: (v) => setState(() => _lineFilter = v),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 12),
|
||||
|
||||
// ── Lista ──────────────────────────────────────────
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
var rows = svc.points.toList();
|
||||
if (_statusFilter != null) {
|
||||
rows =
|
||||
rows.where((p) => p.status == _statusFilter).toList();
|
||||
}
|
||||
if (_lineFilter != null) {
|
||||
rows = rows.where((p) => p.lineId == _lineFilter).toList();
|
||||
}
|
||||
rows.sort((a, b) => a.lineId != b.lineId
|
||||
? a.lineId.compareTo(b.lineId)
|
||||
: a.station.compareTo(b.station));
|
||||
|
||||
if (rows.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Text('Nincs a szűrésnek megfelelő pont.'),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: rows.length,
|
||||
itemBuilder: (context, i) => _row(context, rows[i]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(BuildContext context, StakeoutPoint p) {
|
||||
final isTarget = svc.target.value?.uuid == p.uuid;
|
||||
|
||||
final (color, icon) = switch (p.status) {
|
||||
StakeoutStatus.staked => (Colors.green, Icons.check_circle),
|
||||
StakeoutStatus.skipped => (Colors.grey, Icons.block),
|
||||
StakeoutStatus.pending => (
|
||||
Colors.deepOrange,
|
||||
Icons.radio_button_unchecked
|
||||
),
|
||||
};
|
||||
|
||||
// Távolság a jelenlegi pozíciótól (ha van).
|
||||
String distText = '';
|
||||
if (svc.hasPosition.value) {
|
||||
final dy = p.planEovY - svc.curEovY.value;
|
||||
final dx = p.planEovX - svc.curEovX.value;
|
||||
final d = math.sqrt(dy * dy + dx * dx);
|
||||
distText = d >= 1000
|
||||
? '${(d / 1000).toStringAsFixed(1)} km'
|
||||
: '${d.toStringAsFixed(0)} m';
|
||||
}
|
||||
|
||||
String cm(double? v) => v == null ? '—' : '${(v * 100).toStringAsFixed(0)}';
|
||||
|
||||
final sub = p.status == StakeoutStatus.staked
|
||||
? 'in ${cm(p.devInline)} · cross ${cm(p.devCrossline)}'
|
||||
'${p.devDz != null ? ' · dZ ${cm(p.devDz)}' : ''} cm'
|
||||
: [
|
||||
if (distText.isNotEmpty) distText,
|
||||
if (p.isOffset) 'eltolt (${p.offsetDist?.toStringAsFixed(1)} m)',
|
||||
].join(' · ');
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
selected: isTarget,
|
||||
leading: Icon(icon, color: color, size: 20),
|
||||
title: Text(p.displayId,
|
||||
style: TextStyle(
|
||||
fontWeight: isTarget ? FontWeight.w700 : FontWeight.w500)),
|
||||
subtitle:
|
||||
sub.isEmpty ? null : Text(sub, style: const TextStyle(fontSize: 11)),
|
||||
onTap: () {
|
||||
svc.setTarget(p);
|
||||
Get.back();
|
||||
},
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (a) async {
|
||||
switch (a) {
|
||||
case 'target':
|
||||
svc.setTarget(p);
|
||||
Get.back();
|
||||
case 'skip':
|
||||
await svc.setPointStatus(p, StakeoutStatus.skipped);
|
||||
case 'restore':
|
||||
await svc.setPointStatus(p, StakeoutStatus.pending);
|
||||
case 'delete':
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Pont törlése'),
|
||||
content: Text('${p.displayId} törlése a kitűzési listából?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Törlés')),
|
||||
],
|
||||
));
|
||||
if (ok == true) await svc.deletePoint(p);
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'target', child: Text('Ez legyen a cél')),
|
||||
if (p.status != StakeoutStatus.skipped)
|
||||
const PopupMenuItem(value: 'skip', child: Text('Kihagyás')),
|
||||
if (p.status != StakeoutStatus.pending)
|
||||
const PopupMenuItem(
|
||||
value: 'restore', child: Text('Visszaállítás várakozóra')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('Törlés')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Export dialógus ────────────────────────────────────────────
|
||||
|
||||
Future<void> _exportDialog() async {
|
||||
final hungarian = true.obs;
|
||||
final stakedOnly = false.obs;
|
||||
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Export CSV'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Obx(() => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Magyar Excel-formátum'),
|
||||
subtitle: const Text('pontosvessző + tizedesvessző',
|
||||
style: TextStyle(fontSize: 11)),
|
||||
value: hungarian.value,
|
||||
onChanged: (v) => hungarian.value = v,
|
||||
)),
|
||||
Obx(() => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Csak a kitűzött pontok'),
|
||||
value: stakedOnly.value,
|
||||
onChanged: (v) => stakedOnly.value = v,
|
||||
)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.ios_share, size: 18),
|
||||
label: const Text('Export'),
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
try {
|
||||
await StakeoutExportService.exportAndShare(
|
||||
points: svc.points.toList(),
|
||||
hungarian: hungarian.value,
|
||||
stakedOnly: stakedOnly.value,
|
||||
projectName: ProjectService.to.activeProject.value?.name ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Export', e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_service.dart';
|
||||
|
||||
import '../../../../models/stakeout_point.dart';
|
||||
|
||||
/// Kitűzési térképréteg — a SharedMapWidget children listájába illesztendő.
|
||||
///
|
||||
/// Csak akkor rajzol, ha a StakeoutService aktív (mode == stakeout), így
|
||||
/// feltétel nélkül bent maradhat a rétegek között. Tartalma: vonalanként
|
||||
/// polyline (station-sorrendben), státusz-színezett markerek (koppintás =
|
||||
/// célváltás), vezetővonal a pozíciótól a célig.
|
||||
class StakeoutMapLayers extends StatelessWidget {
|
||||
const StakeoutMapLayers({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Get.isRegistered<StakeoutService>()) return const SizedBox.shrink();
|
||||
|
||||
return Obx(() {
|
||||
final svc = StakeoutService.to;
|
||||
if (!svc.active.value || svc.points.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final points = svc.points;
|
||||
final target = svc.target.value;
|
||||
final g = GnssService.to;
|
||||
final hasPos = svc.hasPosition.value && g.latitude.value != 0;
|
||||
|
||||
// Vonalanként polyline (station-sorrendben, eltolt pontok nélkül).
|
||||
final linePolys = <Polyline>[];
|
||||
for (final line in svc.lines) {
|
||||
final lp = points.where((p) => p.lineId == line && !p.isOffset).toList()
|
||||
..sort((a, b) => a.station.compareTo(b.station));
|
||||
if (lp.length < 2) continue;
|
||||
linePolys.add(Polyline(
|
||||
points: [for (final p in lp) LatLng(p.planLat, p.planLon)],
|
||||
color: Colors.blueGrey.withOpacity(0.5),
|
||||
strokeWidth: 2,
|
||||
));
|
||||
}
|
||||
// Transzverzális az aktuális célnál: a vonalra merőleges segédvonal
|
||||
// ±30 m hosszan, 10 m-enkénti méter-címkékkel — terepi akadálynál
|
||||
// e mentén látszik, hova tolható el a pont.
|
||||
final transversal = <LatLng>[];
|
||||
final transversalTicks = <Marker>[];
|
||||
if (target != null && Get.isRegistered<CoordConverterService>()) {
|
||||
final lb = svc.lineBearingAt(target);
|
||||
if (lb != null) {
|
||||
final perpRad = (lb + 90) * math.pi / 180;
|
||||
LatLng at(double d) {
|
||||
final y = target.planEovY + d * math.sin(perpRad);
|
||||
final x = target.planEovX + d * math.cos(perpRad);
|
||||
final w = CoordConverterService.to.eovToWgsPoint(y, x);
|
||||
return LatLng(w.y, w.x);
|
||||
}
|
||||
|
||||
transversal
|
||||
..add(at(-30))
|
||||
..add(at(30));
|
||||
|
||||
for (final d in const [-30.0, -20.0, -10.0, 10.0, 20.0, 30.0]) {
|
||||
transversalTicks.add(Marker(
|
||||
point: at(d),
|
||||
width: 24,
|
||||
height: 16,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.85),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
d.abs().toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 300 pont fölött a címkék csak a célon (teljesítmény).
|
||||
final showLabels = points.length <= 300;
|
||||
|
||||
return Stack(children: [
|
||||
PolylineLayer(polylines: [
|
||||
if (transversal.isNotEmpty)
|
||||
Polyline(
|
||||
points: transversal,
|
||||
color: Colors.orange.withValues(alpha: 0.8),
|
||||
strokeWidth: 2),
|
||||
...linePolys,
|
||||
if (hasPos && target != null)
|
||||
Polyline(
|
||||
points: [
|
||||
LatLng(g.latitude.value, g.longitude.value),
|
||||
LatLng(target.planLat, target.planLon),
|
||||
],
|
||||
color: Colors.blue,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
]),
|
||||
MarkerLayer(markers: [
|
||||
...transversalTicks,
|
||||
for (final p in points)
|
||||
Marker(
|
||||
point: LatLng(p.planLat, p.planLon),
|
||||
width: 56,
|
||||
height: 40,
|
||||
child: GestureDetector(
|
||||
onTap: () => svc.setTarget(p),
|
||||
child: _StakeoutMarker(
|
||||
point: p,
|
||||
isTarget: p.uuid == target?.uuid,
|
||||
showLabel: showLabels || p.uuid == target?.uuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _StakeoutMarker extends StatelessWidget {
|
||||
final StakeoutPoint point;
|
||||
final bool isTarget;
|
||||
final bool showLabel;
|
||||
const _StakeoutMarker(
|
||||
{required this.point, required this.isTarget, required this.showLabel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (color, icon) = switch (point.status) {
|
||||
StakeoutStatus.staked => (Colors.green, Icons.check_circle),
|
||||
StakeoutStatus.skipped => (Colors.grey, Icons.block),
|
||||
StakeoutStatus.pending => point.isOffset
|
||||
? (Colors.purple, Icons.change_history)
|
||||
: (Colors.deepOrange, Icons.change_history),
|
||||
};
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: isTarget ? 26 : 16, color: isTarget ? Colors.red : color),
|
||||
if (showLabel)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
point.name,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: isTarget ? FontWeight.w700 : FontWeight.w400,
|
||||
color: isTarget ? Colors.red : Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show FontFeature;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
//import 'package:flutter/widget_previews.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_list_sheet.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_settings_dialog.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/widgets/tilt_bubble.dart';
|
||||
import 'package:terepi_seged/routes/app_pages.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_service.dart';
|
||||
|
||||
/// Kitűzési alsó panel — a map_survey nézet overlay-jébe illesztendő
|
||||
/// (mode == MapSurveyMode.stakeout esetén). Kétfázisú: 5 m felett nagy
|
||||
/// irány-nyíl + távolság, alatta céltábla cm-es eltérésekkel. A tárolás,
|
||||
/// kihagyás és transzverzális eltolás gombjai is itt vannak.
|
||||
class StakeoutPanel extends StatelessWidget {
|
||||
const StakeoutPanel({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Get.isRegistered<StakeoutService>()) return const SizedBox.shrink();
|
||||
final svc = StakeoutService.to;
|
||||
|
||||
return Obx(() {
|
||||
// Üres állapot: import-hívás.
|
||||
if (svc.points.isEmpty) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text('Nincsenek kitűzési pontok az aktív '
|
||||
'projektben.'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.STAKEOUT_IMPORT),
|
||||
icon: const Icon(Icons.upload_file, size: 18),
|
||||
label: const Text('Import'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final t = svc.target.value;
|
||||
if (t == null) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Minden pont kitűzve vagy kihagyva. '
|
||||
'(${svc.points.length} pont)'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final within = svc.withinTolerance.value;
|
||||
final progress = svc.lineProgress[t.lineId];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
elevation: 6,
|
||||
color: within
|
||||
? Colors.green.shade50
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: within
|
||||
? const BorderSide(color: Colors.green, width: 2)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── Fejléc ───────────────────────────────────────────
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
onPressed: () => svc.advance(backwards: true),
|
||||
),
|
||||
// A cím rugalmas: szűk helyen ZSUGORODIK (FittedBox),
|
||||
// sosem tördelődik karakterenként.
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
t.displayId,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
if (progress != null)
|
||||
Text(
|
||||
'${progress.staked}/${progress.total} kitűzve'
|
||||
'${t.isOffset ? ' · ELTOLT' : ''}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
onPressed: () => svc.advance(),
|
||||
),
|
||||
const TiltBubble(),
|
||||
PopupMenuButton<StakeoutOrder>(
|
||||
icon: Icon(switch (svc.orderMode.value) {
|
||||
StakeoutOrder.idAsc => Icons.trending_up,
|
||||
StakeoutOrder.idDesc => Icons.trending_down,
|
||||
StakeoutOrder.nearest => Icons.near_me,
|
||||
}),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: 'Sorrend',
|
||||
onSelected: (m) => svc.orderMode.value = m,
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: StakeoutOrder.idAsc,
|
||||
child: Text('Állomás növekvő')),
|
||||
PopupMenuItem(
|
||||
value: StakeoutOrder.idDesc,
|
||||
child: Text('Állomás csökkenő')),
|
||||
PopupMenuItem(
|
||||
value: StakeoutOrder.nearest,
|
||||
child: Text('Legközelebbi')),
|
||||
],
|
||||
),
|
||||
// A ritkán használt műveletek EGY menüben — a fejléc
|
||||
// így keskeny kijelzőn / nagy betűméretnél is kifér.
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: 'További műveletek',
|
||||
onSelected: (a) {
|
||||
switch (a) {
|
||||
case 'list':
|
||||
StakeoutListSheet.show();
|
||||
case 'settings':
|
||||
showStakeoutSettingsDialog();
|
||||
case 'import':
|
||||
Get.toNamed(Routes.STAKEOUT_IMPORT);
|
||||
case 'haptics':
|
||||
svc.hapticsEnabled.toggle();
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(
|
||||
value: 'list', child: Text('Pontlista / export')),
|
||||
const PopupMenuItem(
|
||||
value: 'settings',
|
||||
child: Text('Kitűzési beállítások')),
|
||||
const PopupMenuItem(
|
||||
value: 'import', child: Text('Pontok importja')),
|
||||
CheckedPopupMenuItem(
|
||||
value: 'haptics',
|
||||
checked: svc.hapticsEnabled.value,
|
||||
child: const Text('Haptikus visszajelzés')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// ── Fázis-tartalom ───────────────────────────────────
|
||||
if (!svc.hasPosition.value)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text('Várakozás GNSS pozícióra…',
|
||||
style: TextStyle(color: Colors.orange)),
|
||||
)
|
||||
else if (svc.nearPhase.value)
|
||||
_NearPhase(svc: svc)
|
||||
else
|
||||
_FarPhase(svc: svc),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// ── Gombsor ──────────────────────────────────────────
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(svc.nearPhase.value ? Icons.map : Icons.adjust),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
tooltip:
|
||||
svc.nearPhase.value ? 'Térkép-nézet' : 'Céltábla-nézet',
|
||||
onPressed: svc.togglePhase,
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: svc.skipCurrent,
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text('Kihagy'),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _offsetDialog(svc),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
icon: const Icon(Icons.alt_route, size: 18),
|
||||
label: const Text('Eltolás'),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Expanded: a Tárol tölti ki a maradék helyet —
|
||||
// fix-szélesség-összeg helyett, így nincs overflow.
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed:
|
||||
svc.isAveraging.value ? null : () => _store(svc),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: within ? Colors.green : null,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
icon: svc.isAveraging.value
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.push_pin, size: 18),
|
||||
label: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(svc.isAveraging.value
|
||||
? 'Átlagolás ${svc.averagingElapsed.value}/'
|
||||
'${svc.averagingSeconds.value} s'
|
||||
: 'Tárol'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Tárolás — figyelmeztetés, ha nem RTK fixed (de NEM tiltás)
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<void> _store(StakeoutService svc) async {
|
||||
if (svc.target.value == null || !svc.hasPosition.value) return;
|
||||
|
||||
final quality = GnssService.to.gpsQuality.value;
|
||||
if (quality != 4) {
|
||||
final proceed = await Get.dialog<bool>(AlertDialog(
|
||||
icon: const Icon(Icons.warning_amber, color: Colors.orange),
|
||||
title: const Text('Nincs RTK fixed'),
|
||||
content: Text(
|
||||
'A jelenlegi megoldás: ${_fixLabel(quality)}.\n'
|
||||
'A pont tárolható, de a pontosság csökkent lehet — a '
|
||||
'fix-minőség bekerül a jegyzőkönyvbe.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Tárolás így is')),
|
||||
],
|
||||
));
|
||||
if (proceed != true) return;
|
||||
}
|
||||
|
||||
final stored = await svc.storeCurrent();
|
||||
if (stored == null) return;
|
||||
|
||||
Get.snackbar(
|
||||
'✓ ${stored.displayId} kitűzve',
|
||||
'inline ${_cm(stored.devInline)} · crossline ${_cm(stored.devCrossline)}'
|
||||
'${stored.devDz != null ? ' · dZ ${_cm(stored.devDz)}' : ''}'
|
||||
' · ${_fixLabel(stored.fixQuality ?? 0)}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFF2E7D32),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
svc.advance();
|
||||
}
|
||||
|
||||
Future<void> _offsetDialog(StakeoutService svc) async {
|
||||
final distCtrl = TextEditingController(text: '2');
|
||||
final toRight = true.obs;
|
||||
final both = false.obs;
|
||||
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Eltolt pont (transzverzális)'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'A vonalra merőlegesen, a tervponttól mért távolságra. '
|
||||
'Az eltolás iránya és nagysága bekerül a jegyzőkönyvbe.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final d in ['0.5', '1', '2', '5'])
|
||||
ActionChip(
|
||||
label: Text('$d m'), onPressed: () => distCtrl.text = d),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: distCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Távolság (m)', isDense: true),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(() => SegmentedButton<bool>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: false,
|
||||
label: Text('Balra'),
|
||||
icon: Icon(Icons.west)),
|
||||
ButtonSegment(
|
||||
value: true,
|
||||
label: Text('Jobbra'),
|
||||
icon: Icon(Icons.east)),
|
||||
],
|
||||
selected: {toRight.value},
|
||||
onSelectionChanged: (s) => toRight.value = s.first,
|
||||
)),
|
||||
Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Pár létrehozása (bal ÉS jobb)'),
|
||||
subtitle: const Text('az akadály két oldalára',
|
||||
style: TextStyle(fontSize: 11)),
|
||||
value: both.value,
|
||||
onChanged: (v) => both.value = v ?? false,
|
||||
))
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final d = double.tryParse(distCtrl.text.replaceAll(',', '.'));
|
||||
if (d == null || d <= 0) return;
|
||||
|
||||
// Megrendelői offset-korlát ellenőrzése (beállításokból).
|
||||
if (svc.maxOffset.value > 0 && d > svc.maxOffset.value) {
|
||||
final ok = await Get.dialog<bool>(AlertDialog(
|
||||
icon: const Icon(Icons.warning_amber, color: Colors.orange),
|
||||
title: const Text('Offset-korlát túllépése'),
|
||||
content: Text('A megadott ${d.toStringAsFixed(1)} m nagyobb '
|
||||
'a beállított max. ${svc.maxOffset.value.toStringAsFixed(1)} m '
|
||||
'crossline eltolásnál.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Mégis')),
|
||||
],
|
||||
));
|
||||
if (ok != true) return;
|
||||
}
|
||||
|
||||
Get.back();
|
||||
if (both.value) {
|
||||
// Előbb az ellenoldali, aztán a választott — így a
|
||||
// választott oldal lesz az aktív cél.
|
||||
await svc.createOffset(dist: d, toRight: !toRight.value);
|
||||
await svc.createOffset(dist: d, toRight: toRight.value);
|
||||
Get.snackbar('Eltolt pontpár létrehozva',
|
||||
'±${d.toStringAsFixed(2)} m a vonalra merőlegesen.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
} else {
|
||||
final p = await svc.createOffset(dist: d, toRight: toRight.value);
|
||||
if (p != null) {
|
||||
Get.snackbar(
|
||||
'Eltolt pont létrehozva',
|
||||
'${p.displayId} · ${d.toStringAsFixed(2)} m '
|
||||
'${toRight.value ? 'jobbra' : 'balra'} — ez az új cél.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Létrehozás'),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
static String _cm(double? v) => v == null
|
||||
? '—'
|
||||
: v.abs() < 1
|
||||
? '${(v * 100).toStringAsFixed(0)} cm'
|
||||
: '${v.toStringAsFixed(2)} m';
|
||||
|
||||
static String _fixLabel(int q) => switch (q) {
|
||||
4 => 'RTK FIXED',
|
||||
5 => 'RTK FLOAT',
|
||||
2 => 'DGPS',
|
||||
1 => 'GPS',
|
||||
0 => 'nincs fix',
|
||||
_ => 'fix: $q',
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Távoli fázis: nagy nyíl + távolság
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class _FarPhase extends StatelessWidget {
|
||||
final StakeoutService svc;
|
||||
const _FarPhase({required this.svc});
|
||||
|
||||
static const _compass = ['É', 'ÉK', 'K', 'DK', 'D', 'DNy', 'Ny', 'ÉNy'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final heading = svc.travelHeading.value;
|
||||
final bearing = svc.bearingToTarget.value;
|
||||
final compass = _compass[((bearing + 22.5) % 360 ~/ 45)];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// A nyíl a HALADÁSI IRÁNYHOZ képest forog; álló helyzetben
|
||||
// iránytű-ikon + égtáj-szöveg.
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: heading != null
|
||||
? Transform.rotate(
|
||||
angle: (bearing - heading) * math.pi / 180,
|
||||
child: const Icon(Icons.navigation,
|
||||
size: 58, color: Colors.blue),
|
||||
)
|
||||
: const Icon(Icons.explore, size: 52, color: Colors.blueGrey),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_fmtDist(svc.distance.value),
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
heading != null
|
||||
? '$compass felé (${bearing.toStringAsFixed(0)}°)'
|
||||
: 'Indulj el — a nyíl követi a haladásod · $compass',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
if (svc.devDz.value != null)
|
||||
Text(
|
||||
'dZ: ${svc.devDz.value! >= 0 ? 'fel' : 'le'} '
|
||||
'${_fmtDist(svc.devDz.value!.abs())}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static String _fmtDist(double d) => d >= 1000
|
||||
? '${(d / 1000).toStringAsFixed(2)} km'
|
||||
: d >= 10
|
||||
? '${d.toStringAsFixed(1)} m'
|
||||
: '${d.toStringAsFixed(2)} m';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Közeli fázis: céltábla + cm-es eltérések
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class _NearPhase extends StatelessWidget {
|
||||
final StakeoutService svc;
|
||||
const _NearPhase({required this.svc});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final fwd = svc.devForward.value;
|
||||
final right = svc.devRight.value;
|
||||
final dz = svc.devDz.value;
|
||||
final within = svc.withinTolerance.value;
|
||||
final ne = svc.deviationMode.value == DeviationMode.northEast;
|
||||
|
||||
final (fwdLabel, rightLabel) = switch (svc.deviationMode.value) {
|
||||
DeviationMode.line => ('Inline', 'Crossline'),
|
||||
DeviationMode.travel => ('Előre/hátra', 'Jobbra/balra'),
|
||||
DeviationMode.northEast => ('Észak', 'Kelet'),
|
||||
};
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SegmentedButton<DeviationMode>(
|
||||
segments: [
|
||||
for (final m in DeviationMode.values)
|
||||
ButtonSegment(
|
||||
value: m,
|
||||
label: Text(m.label, style: const TextStyle(fontSize: 10))),
|
||||
],
|
||||
selected: {svc.deviationMode.value},
|
||||
onSelectionChanged: (s) => svc.deviationMode.value = s.first,
|
||||
showSelectedIcon: false,
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
height: 140,
|
||||
child: CustomPaint(
|
||||
painter: _BullseyePainter(
|
||||
devForward: fwd,
|
||||
devRight: right,
|
||||
tolerance: svc.toleranceXY.value,
|
||||
within: within,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_DevRow(
|
||||
label: fwdLabel,
|
||||
value: fwd,
|
||||
posText: ne ? 'É' : 'előre',
|
||||
negText: ne ? 'D' : 'hátra'),
|
||||
_DevRow(
|
||||
label: rightLabel,
|
||||
value: right,
|
||||
posText: ne ? 'K' : 'jobbra',
|
||||
negText: ne ? 'Ny' : 'balra'),
|
||||
if (dz != null)
|
||||
_DevRow(
|
||||
label: 'Magasság',
|
||||
value: dz,
|
||||
posText: 'fel',
|
||||
negText: 'le'),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
within
|
||||
? '✓ Tűrésen belül '
|
||||
'(${(svc.toleranceXY.value * 100).toStringAsFixed(0)} cm)'
|
||||
: 'Távolság: '
|
||||
'${(svc.distance.value * 100).toStringAsFixed(0)} cm',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: within ? Colors.green : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _DevRow extends StatelessWidget {
|
||||
final String label;
|
||||
final double value;
|
||||
final String posText;
|
||||
final String negText;
|
||||
const _DevRow(
|
||||
{required this.label,
|
||||
required this.value,
|
||||
required this.posText,
|
||||
required this.negText});
|
||||
|
||||
//@Preview(name: 'Example')
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cm = value.abs() * 100;
|
||||
final txt = cm < 100
|
||||
? '${cm.toStringAsFixed(0)} cm'
|
||||
: '${value.abs().toStringAsFixed(2)} m';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 78,
|
||||
child: Text(label,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
||||
),
|
||||
Text(
|
||||
'${value >= 0 ? posText : negText} $txt',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Céltábla: középen a CÉL, a kék pötty a MI pozíciónk a célhoz képest.
|
||||
/// A "felfelé" tengely az aktuális eltérés-mód "előre" iránya.
|
||||
class _BullseyePainter extends CustomPainter {
|
||||
final double devForward;
|
||||
final double devRight;
|
||||
final double tolerance;
|
||||
final bool within;
|
||||
|
||||
_BullseyePainter({
|
||||
required this.devForward,
|
||||
required this.devRight,
|
||||
required this.tolerance,
|
||||
required this.within,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final c = Offset(size.width / 2, size.height / 2);
|
||||
final maxR = size.width / 2 - 4;
|
||||
const viewRadiusM = 1.2;
|
||||
final scale = maxR / viewRadiusM;
|
||||
|
||||
final ring = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..color = Colors.grey.shade400;
|
||||
|
||||
for (final r in [1.0, 0.5]) {
|
||||
canvas.drawCircle(c, r * scale, ring);
|
||||
}
|
||||
canvas.drawCircle(
|
||||
c,
|
||||
tolerance * scale,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2
|
||||
..color = within ? Colors.green : Colors.grey.shade600);
|
||||
|
||||
canvas.drawLine(Offset(c.dx - maxR, c.dy), Offset(c.dx + maxR, c.dy), ring);
|
||||
canvas.drawLine(Offset(c.dx, c.dy - maxR), Offset(c.dx, c.dy + maxR), ring);
|
||||
|
||||
canvas.drawCircle(
|
||||
c, 4, Paint()..color = within ? Colors.green : Colors.red);
|
||||
|
||||
// A MI pozíciónk a célhoz képest: offset = −(eltérés) az
|
||||
// (előre, jobbra) bázisban; képernyő-y felfelé = előre.
|
||||
var px = -devRight * scale;
|
||||
var py = devForward * scale;
|
||||
final d = math.sqrt(px * px + py * py);
|
||||
if (d > maxR) {
|
||||
px = px / d * maxR;
|
||||
py = py / d * maxR;
|
||||
}
|
||||
final me = c + Offset(px, py);
|
||||
canvas.drawCircle(me, 7, Paint()..color = Colors.blue);
|
||||
canvas.drawCircle(
|
||||
me,
|
||||
7,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2
|
||||
..color = Colors.white);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_BullseyePainter old) =>
|
||||
old.devForward != devForward ||
|
||||
old.devRight != devRight ||
|
||||
old.within != within ||
|
||||
old.tolerance != tolerance;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../services/stakeout_service.dart';
|
||||
|
||||
/// Kitűzési beállítások dialógus — a panel fogaskerék-ikonjáról.
|
||||
/// Minden érték a StakeoutService Rx-eiben él, és a service perzisztálja
|
||||
/// (SharedPreferences), így app-újraindítás után is megmaradnak.
|
||||
Future<void> showStakeoutSettingsDialog() async {
|
||||
final svc = StakeoutService.to;
|
||||
|
||||
final tolCtrl = TextEditingController(
|
||||
text: (svc.toleranceXY.value * 100).toStringAsFixed(0));
|
||||
final maxOffCtrl = TextEditingController(
|
||||
text: svc.maxOffset.value > 0
|
||||
? svc.maxOffset.value.toStringAsFixed(1)
|
||||
: '');
|
||||
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Kitűzési beállítások'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: tolCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tűrés (cm)',
|
||||
helperText: 'A céltábla zöld köre és a "tűrésen belül" jelzés',
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text('Átlagolás tároláskor',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
Obx(() => SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 0, label: Text('Ki')),
|
||||
ButtonSegment(value: 3, label: Text('3 s')),
|
||||
ButtonSegment(value: 5, label: Text('5 s')),
|
||||
ButtonSegment(value: 10, label: Text('10 s')),
|
||||
],
|
||||
selected: {svc.averagingSeconds.value},
|
||||
onSelectionChanged: (s) => svc.averagingSeconds.value = s.first,
|
||||
showSelectedIcon: false,
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Bekapcsolva a "Tárol" a megadott ideig gyűjti és átlagolja '
|
||||
'a pozíciókat — csökkenti a pillanatnyi zaj hatását.',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(() => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Haptikus visszajelzés'),
|
||||
subtitle: const Text('közeledve sűrűsödő rezgés',
|
||||
style: TextStyle(fontSize: 11)),
|
||||
value: svc.hapticsEnabled.value,
|
||||
onChanged: (v) => svc.hapticsEnabled.value = v,
|
||||
)),
|
||||
Obx(() => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: const Text('Hangjelzés'),
|
||||
subtitle: const Text('kattanás a pulzusokkal, hang a tűrésnél',
|
||||
style: TextStyle(fontSize: 11)),
|
||||
value: svc.soundEnabled.value,
|
||||
onChanged: (v) => svc.soundEnabled.value = v,
|
||||
)),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: maxOffCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Max. crossline eltolás (m, üres = nincs korlát)',
|
||||
labelStyle: TextStyle(fontSize: 12),
|
||||
helperText: 'A specifikáció szerinti offset-korlát — '
|
||||
'e fölött az eltolás-dialógus figyelmeztet.',
|
||||
helperMaxLines: 2,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final tolCm = double.tryParse(tolCtrl.text.replaceAll(',', '.'));
|
||||
if (tolCm != null && tolCm > 0) {
|
||||
svc.toleranceXY.value = tolCm / 100.0;
|
||||
}
|
||||
final maxOff = double.tryParse(maxOffCtrl.text.replaceAll(',', '.'));
|
||||
svc.maxOffset.value = maxOff ?? 0;
|
||||
|
||||
await svc.saveSettings();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Mentés'),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../services/tilt_service.dart';
|
||||
|
||||
/// Rúd-libella buborék — kis kör alakú widget a kitűzőpanel fejlécébe.
|
||||
///
|
||||
/// Színküszöbök a DŐLÉSBŐL SZÁMOLT VÍZSZINTES HIBA szerint (a rúdmagasság
|
||||
/// figyelembevételével), mert terepen ez a beszédes érték:
|
||||
/// zöld < 1,5 cm · sárga < 3,5 cm · piros felette.
|
||||
/// Mozgás közben és kalibrálatlanul szürke/kérdőjeles.
|
||||
/// Koppintásra részletes nézet: nagy buborék, számok, rúdmagasság,
|
||||
/// vezetett (1 vagy 2 lépéses) kalibráció.
|
||||
class TiltBubble extends StatelessWidget {
|
||||
const TiltBubble({super.key, this.size = 34});
|
||||
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Get.isRegistered<TiltService>()) return const SizedBox.shrink();
|
||||
|
||||
return Obx(() {
|
||||
final t = TiltService.to;
|
||||
final inactive = !t.isCalibrated.value || t.isMoving.value;
|
||||
|
||||
final errCm = t.horizontalErrorCm;
|
||||
final color = inactive
|
||||
? Colors.grey
|
||||
: errCm < 1.5
|
||||
? Colors.green
|
||||
: errCm < 3.5
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
return Tooltip(
|
||||
message: !t.isCalibrated.value
|
||||
? 'Libella: nincs kalibrálva — koppints!'
|
||||
: t.isMoving.value
|
||||
? 'Libella: mozgásban'
|
||||
: 'Dőlés ${t.tiltDeg.value.toStringAsFixed(1)}° '
|
||||
'≈ ${errCm.toStringAsFixed(1)} cm',
|
||||
child: InkWell(
|
||||
onTap: () => _showDetail(context),
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: _BubbleFace(
|
||||
size: size,
|
||||
color: color,
|
||||
inactive: inactive,
|
||||
uncalibrated: !t.isCalibrated.value,
|
||||
// A buborék a "lejtés" irányába csúszik — skála: 1 fok
|
||||
// dőlés ≈ a sugár ~25%-a, telítésig.
|
||||
dx: t.bubbleX.value,
|
||||
dy: t.bubbleY.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Részletes nézet + kalibráció
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<void> _showDetail(BuildContext context) async {
|
||||
final t = TiltService.to;
|
||||
final poleCtrl =
|
||||
TextEditingController(text: t.poleHeight.value.toStringAsFixed(2));
|
||||
|
||||
await Get.dialog(AlertDialog(
|
||||
title: const Text('Rúd-libella'),
|
||||
content: SingleChildScrollView(
|
||||
child: Obx(() {
|
||||
final errCm = t.horizontalErrorCm;
|
||||
final inactive = !t.isCalibrated.value || t.isMoving.value;
|
||||
final color = inactive
|
||||
? Colors.grey
|
||||
: errCm < 1.5
|
||||
? Colors.green
|
||||
: errCm < 3.5
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
height: 140,
|
||||
child: _BubbleFace(
|
||||
size: 140,
|
||||
color: color,
|
||||
inactive: inactive,
|
||||
uncalibrated: !t.isCalibrated.value,
|
||||
dx: t.bubbleX.value,
|
||||
dy: t.bubbleY.value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
!t.isCalibrated.value
|
||||
? 'Nincs kalibrálva'
|
||||
: t.isMoving.value
|
||||
? 'Mozgásban — állj meg a leolvasáshoz'
|
||||
: '${t.tiltDeg.value.toStringAsFixed(2)}° ≈ '
|
||||
'${errCm.toStringAsFixed(1)} cm vízszintes hiba',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: inactive ? Colors.grey : color),
|
||||
),
|
||||
Text(
|
||||
'@ ${t.poleHeight.value.toStringAsFixed(2)} m rúdmagasság',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: poleCtrl,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rúdmagasság (m)',
|
||||
isDense: true,
|
||||
),
|
||||
onSubmitted: (v) {
|
||||
final m = double.tryParse(v.replaceAll(',', '.'));
|
||||
if (m != null) t.setPoleHeight(m);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'A jelzés csak akkor érvényes, ha a telefon MEREVEN, '
|
||||
'tartóban van a rúdon. Tartó-áthelyezés után kalibrálj újra.',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: [
|
||||
if (TiltService.to.isCalibrated.value)
|
||||
TextButton(
|
||||
onPressed: () => TiltService.to.clearCalibration(),
|
||||
child: const Text('Kalibráció törlése'),
|
||||
),
|
||||
TextButton(onPressed: Get.back, child: const Text('Bezár')),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.architecture, size: 18),
|
||||
label: const Text('Kalibrálás'),
|
||||
onPressed: () async {
|
||||
final m = double.tryParse(poleCtrl.text.replaceAll(',', '.'));
|
||||
if (m != null) await TiltService.to.setPoleHeight(m);
|
||||
Get.back();
|
||||
await _calibrationFlow();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/// Vezetett kalibráció: 1. lépés kötelező, 2. (180°-os átforgatásos)
|
||||
/// opcionális — utóbbi a tartó ferdeségét is kiejti.
|
||||
Future<void> _calibrationFlow() async {
|
||||
final t = TiltService.to;
|
||||
|
||||
final step1Ok = await _captureStep(
|
||||
title: 'Kalibráció — 1/2',
|
||||
text: 'Állítsd a rudat FÜGGŐLEGESRE a szelencés libella szerint, '
|
||||
'és tartsd mozdulatlanul. A mérés 2 másodpercig tart.',
|
||||
);
|
||||
if (step1Ok != true) return;
|
||||
final v1 = await t.captureVector();
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
final doStep2 = await Get.dialog<bool>(AlertDialog(
|
||||
title: const Text('Kalibráció — pontosítás?'),
|
||||
content: const Text(
|
||||
'A pontosabb (tartó-ferdeséget is kiejtő) kalibrációhoz forgasd '
|
||||
'át a telefont a RÚD TENGELYE körül 180°-kal (a rúd maradjon '
|
||||
'függőleges), majd folytasd. Ki is hagyhatod — akkor az 1. '
|
||||
'lépés lesz a referencia.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Kihagyom')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Átforgattam — folytatás')),
|
||||
],
|
||||
));
|
||||
|
||||
if (doStep2 == true) {
|
||||
final v2 = await t.captureVector();
|
||||
HapticFeedback.mediumImpact();
|
||||
await t.saveReference(v1, v2);
|
||||
} else {
|
||||
await t.saveReference(v1);
|
||||
}
|
||||
|
||||
Get.snackbar('Libella', 'Kalibráció kész — a buborék él.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFF2E7D32),
|
||||
colorText: const Color(0xFFFFFFFF));
|
||||
}
|
||||
|
||||
Future<bool?> _captureStep({required String title, required String text}) {
|
||||
return Get.dialog<bool>(AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(text),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Indítás')),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class _BubbleFace extends StatelessWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
final bool inactive;
|
||||
final bool uncalibrated;
|
||||
final double dx;
|
||||
final double dy;
|
||||
|
||||
const _BubbleFace({
|
||||
required this.size,
|
||||
required this.color,
|
||||
required this.inactive,
|
||||
required this.uncalibrated,
|
||||
required this.dx,
|
||||
required this.dy,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Skála: sin(4°) dőlésnél ér a széléhez.
|
||||
final r = size / 2;
|
||||
final scale = (r - size * 0.14) / 0.07;
|
||||
var ox = dx * scale;
|
||||
var oy = -dy * scale; // képernyő-y lefelé nő
|
||||
final d = (ox * ox + oy * oy);
|
||||
final maxR = r - size * 0.14;
|
||||
if (d > maxR * maxR) {
|
||||
final f = maxR / math.sqrt(d);
|
||||
ox *= f;
|
||||
oy *= f;
|
||||
}
|
||||
|
||||
return CustomPaint(
|
||||
painter: _BubblePainter(
|
||||
color: color,
|
||||
inactive: inactive,
|
||||
ox: ox,
|
||||
oy: oy,
|
||||
dotRadius: size * 0.13,
|
||||
),
|
||||
child: uncalibrated
|
||||
? Center(
|
||||
child: Text('?',
|
||||
style: TextStyle(
|
||||
fontSize: size * 0.45,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey)))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BubblePainter extends CustomPainter {
|
||||
final Color color;
|
||||
final bool inactive;
|
||||
final double ox;
|
||||
final double oy;
|
||||
final double dotRadius;
|
||||
|
||||
_BubblePainter({
|
||||
required this.color,
|
||||
required this.inactive,
|
||||
required this.ox,
|
||||
required this.oy,
|
||||
required this.dotRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final c = Offset(size.width / 2, size.height / 2);
|
||||
final r = size.width / 2;
|
||||
|
||||
canvas.drawCircle(
|
||||
c,
|
||||
r - 1,
|
||||
Paint()
|
||||
..color = color.withOpacity(inactive ? 0.06 : 0.12)
|
||||
..style = PaintingStyle.fill);
|
||||
canvas.drawCircle(
|
||||
c,
|
||||
r - 1,
|
||||
Paint()
|
||||
..color = color.withOpacity(inactive ? 0.5 : 0.9)
|
||||
..strokeWidth = 1.6
|
||||
..style = PaintingStyle.stroke);
|
||||
// Belső célkör
|
||||
canvas.drawCircle(
|
||||
c,
|
||||
r * 0.35,
|
||||
Paint()
|
||||
..color = color.withOpacity(0.5)
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke);
|
||||
|
||||
if (!inactive) {
|
||||
canvas.drawCircle(c + Offset(ox, oy), dotRadius, Paint()..color = color);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_BubblePainter old) =>
|
||||
old.ox != ox ||
|
||||
old.oy != oy ||
|
||||
old.color != color ||
|
||||
old.inactive != inactive;
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_geojson/flutter_map_geojson.dart';
|
||||
// import 'package:flutter_map_geojson/flutter_map_geojson.dart';
|
||||
import 'package:flutter_map_polywidget/flutter_map_polywidget.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/utils/utils.dart';
|
||||
import 'package:intl/date_time_patterns.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:location/location.dart';
|
||||
@@ -20,6 +21,8 @@ import 'package:nmea/nmea.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart'
|
||||
as permission_handler;
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:terepi_seged/controls/geojson_parser.dart';
|
||||
import 'package:terepi_seged/eov/convert_coordinate.dart';
|
||||
import 'package:terepi_seged/eov/eov.dart';
|
||||
import 'package:terepi_seged/gnss_sentences/gngga.dart';
|
||||
@@ -28,12 +31,13 @@ import 'package:terepi_seged/gnss_sentences/gnrmc.dart';
|
||||
import 'package:terepi_seged/models/point_to_measure.dart';
|
||||
import 'package:terepi_seged/models/point_with_description_model.dart';
|
||||
import 'package:proj4dart/proj4dart.dart' as proj4;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class NavigationViewController extends GetxController {
|
||||
// String gpsAddress = "E8:31:CD:14:8B:B2";
|
||||
// String gpsAddress = "98:CD:AC:62:FF:4E";
|
||||
RxString gpsAddress = "98:CD:AC:62:FF:36".obs;
|
||||
String gpsName = "TiGNSS Rover-FF4E";
|
||||
RxString gpsName = "TiGNSS Rover-FF4E".obs;
|
||||
// String gpsName = "TiGNSS Rover-8BB2";
|
||||
static const double maxZoomValue = 25.0;
|
||||
Rx<bool> gpsIsConnected = false.obs;
|
||||
@@ -118,10 +122,20 @@ class NavigationViewController extends GetxController {
|
||||
final CollectionReference _vibratorTracker =
|
||||
FirebaseFirestore.instance.collection('vibratorTracker');
|
||||
DateTime lastGpsDataSaveTime = DateTime(2000, 1, 1, 0, 0, 0);
|
||||
int vehicleNumber = 2;
|
||||
RxInt vehicleNumber = 5.obs;
|
||||
late Location internalGpsLogger;
|
||||
Location internalGpsLocation = Location();
|
||||
late StreamSubscription<LocationData> internalGpsLocationSubscription;
|
||||
late StreamSubscription<LocationData>? internalGpsLocationSubscription;
|
||||
|
||||
late List<Polyline<Object>> pathLayer = [];
|
||||
|
||||
RxString ntripUserName = "".obs;
|
||||
RxString ntripPassword = "".obs;
|
||||
late SharedPreferences prefs;
|
||||
|
||||
late AuthResponse authResponse;
|
||||
late Session? session;
|
||||
late User? user;
|
||||
|
||||
// late SMIBool gpsTrigger;
|
||||
// late StateMachineController riveGpsIconController;
|
||||
@@ -146,13 +160,18 @@ class NavigationViewController extends GetxController {
|
||||
..registerTalkerSentence("GST", (line) => Gngst(raw: line));
|
||||
|
||||
mapController = MapController();
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
|
||||
authResponse = await Supabase.instance.client.auth
|
||||
.signInWithPassword(email: 'test.elek.1@email.hu', password: 'demo');
|
||||
session = authResponse.session;
|
||||
user = authResponse.user;
|
||||
|
||||
// riveGpsIconController = RiveUtils.getRiveController(Artboard(),
|
||||
// stateMachineName: "gps_Interactivity");
|
||||
// gpsTrigger = riveGpsIconController.findSMI("active");
|
||||
|
||||
mapIsInitialized.value = true;
|
||||
vehicleNumberController.text = vehicleNumber.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -175,7 +194,7 @@ class NavigationViewController extends GetxController {
|
||||
// internalGpsLogTimer.cancel();
|
||||
// }
|
||||
|
||||
internalGpsLocationSubscription.cancel();
|
||||
internalGpsLocationSubscription!.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -184,8 +203,8 @@ class NavigationViewController extends GetxController {
|
||||
|
||||
_getInitialLocation();
|
||||
|
||||
String data = await rootBundle.loadString('assets/Files/kozmuvek.geojson');
|
||||
parser.parseGeoJsonAsString(data);
|
||||
// String data = await rootBundle.loadString('assets/Files/kozmuvek.geojson');
|
||||
// parser.parseGeoJsonAsString(data);
|
||||
|
||||
if (await permission_handler.Permission.storage.isGranted) {
|
||||
print("Storage permission is ok ...");
|
||||
@@ -207,6 +226,37 @@ class NavigationViewController extends GetxController {
|
||||
} else {
|
||||
print("No external storage permission");
|
||||
}
|
||||
if (prefs.containsKey('gpsAddress')) {
|
||||
var address = prefs.getString('gpsAddress');
|
||||
if (address != null) {
|
||||
gpsAddress.value = address;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('gpsName')) {
|
||||
var name = prefs.getString('gpsName');
|
||||
if (name != null) {
|
||||
gpsName.value = name;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('ntripUserName')) {
|
||||
var userName = prefs.getString('ntripUserName');
|
||||
if (userName != null) {
|
||||
ntripUserName.value = userName;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('ntripPassword')) {
|
||||
var password = prefs.getString('ntripPassword');
|
||||
if (password != null) {
|
||||
ntripPassword.value = password;
|
||||
}
|
||||
}
|
||||
if (prefs.containsKey('vehicleNumber')) {
|
||||
var vehicleNum = prefs.getInt('vehicleNumber');
|
||||
if (vehicleNum != null) {
|
||||
vehicleNumber.value = vehicleNum;
|
||||
}
|
||||
}
|
||||
vehicleNumberController.text = vehicleNumber.toString();
|
||||
}
|
||||
|
||||
directory = await getExternalStorageDirectory();
|
||||
@@ -256,7 +306,7 @@ class NavigationViewController extends GetxController {
|
||||
await internalGpsLocation.changeSettings(interval: 10000);
|
||||
|
||||
internalGpsLocationSubscription =
|
||||
internalGpsLogger.onLocationChanged.listen((LocationData value) {
|
||||
internalGpsLogger.onLocationChanged.listen((LocationData value) async {
|
||||
double? longitude;
|
||||
double? latitude;
|
||||
double? accuracy;
|
||||
@@ -276,16 +326,29 @@ class NavigationViewController extends GetxController {
|
||||
internalGpsFile.writeAsString(
|
||||
"$vehicleNumber;$time;$latitude;$longitude;$altitude;$accuracy\r\n");
|
||||
|
||||
_vibratorTracker.add({
|
||||
"gpsType": 0,
|
||||
"vibratorNumber": vehicleNumber,
|
||||
"gpsDateTime": time,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"altitude": altitude,
|
||||
"horizontalError": accuracy,
|
||||
"speed": speed,
|
||||
"createdAt": DateTime.timestamp()
|
||||
// _vibratorTracker.add({
|
||||
// "gpsType": 0,
|
||||
// "vibratorNumber": vehicleNumber.value,
|
||||
// "gpsDateTime": time,
|
||||
// "latitude": latitude,
|
||||
// "longitude": longitude,
|
||||
// "altitude": altitude,
|
||||
// "horizontalError": accuracy,
|
||||
// "speed": speed,
|
||||
// "createdAt": DateTime.timestamp()
|
||||
// });
|
||||
|
||||
await Supabase.instance.client
|
||||
.from('TerepiSeged_VibratorTracker')
|
||||
.insert({
|
||||
'gpsType': 0,
|
||||
'vibratorNumber': vehicleNumber.value,
|
||||
'gpsDateTime': time.toIso8601String(),
|
||||
'latitude': gpsLatitude.value,
|
||||
'longitude': gpsLongitude.value,
|
||||
'altitude': gpsAltitude.value,
|
||||
'horizontalError': accuracy,
|
||||
'speed': speed
|
||||
});
|
||||
});
|
||||
|
||||
@@ -581,14 +644,17 @@ class NavigationViewController extends GetxController {
|
||||
|
||||
if (DateTime.now().difference(lastGpsDataSaveTime).inSeconds >= 10 &&
|
||||
gpsQuality.value > 0) {
|
||||
lastGpsDataSaveTime = DateTime.now();
|
||||
|
||||
_savePositionToDatabase();
|
||||
print("Save to databases: ${lastGpsDataSaveTime}");
|
||||
}
|
||||
}
|
||||
|
||||
void _savePositionToDatabase() {
|
||||
void _savePositionToDatabase() async {
|
||||
_vibratorTracker.add({
|
||||
"gpsType": 1,
|
||||
"vibratorNumber": vehicleNumber,
|
||||
"vibratorNumber": vehicleNumber.value,
|
||||
"gpsQuality": gpsQuality.value,
|
||||
"gpsDateTime": gpsDateTime.value,
|
||||
"latitude": gpsLatitude.value,
|
||||
@@ -601,7 +667,25 @@ class NavigationViewController extends GetxController {
|
||||
"antennaHeight": double.parse(gpsHeightController.text),
|
||||
"createdAt": DateTime.timestamp()
|
||||
});
|
||||
lastGpsDataSaveTime = DateTime.now();
|
||||
|
||||
final res = await Supabase.instance.client
|
||||
.from('TerepiSeged_VibratorTracker')
|
||||
.insert({
|
||||
"gpsType": 1,
|
||||
"vibratorNumber": vehicleNumber.value,
|
||||
"gpsQuality": gpsQuality.value,
|
||||
"gpsDateTime": gpsDateTime.value.toIso8601String(),
|
||||
"latitude": gpsLatitude.value,
|
||||
"longitude": gpsLongitude.value,
|
||||
"altitude": gpsAltitude.value,
|
||||
"eovY": eov.value.Y,
|
||||
"eovX": eov.value.X,
|
||||
"horizontalError": max(gpsLatitudeError.value, gpsLongitudeError.value),
|
||||
"verticalError": gpsAltitudeError.value,
|
||||
// "heightOfGeoid": gpsGeoidSeparation.value,
|
||||
"poleHeight": double.parse(gpsHeightController.text)
|
||||
}).select();
|
||||
print(res);
|
||||
}
|
||||
|
||||
String getGpsQualityIndicator({required int quality}) {
|
||||
@@ -896,15 +980,15 @@ class NavigationViewController extends GetxController {
|
||||
width: 15.0,
|
||||
height: 15.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
color: Colors.yellow,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(width: 1.0, color: Colors.black)),
|
||||
)));
|
||||
|
||||
pointsToMeasureLabel.add(PolyWidget(
|
||||
center: LatLng(wgsCoord.y + 0.0000075, wgsCoord.x + 0.0000075),
|
||||
widthInMeters: 3,
|
||||
heightInMeters: 3,
|
||||
center: LatLng(wgsCoord.y + 0.0000275, wgsCoord.x + 0.0000275),
|
||||
widthInMeters: 28,
|
||||
heightInMeters: 28,
|
||||
// constraints: const BoxConstraints(
|
||||
// minWidth: 250,
|
||||
// maxWidth: 350,
|
||||
@@ -934,6 +1018,24 @@ class NavigationViewController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void ReadPathFromFile() async {
|
||||
File? file;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
|
||||
if (result != null) {
|
||||
file = File(result.files.single.path!);
|
||||
} else {
|
||||
print("No file selected");
|
||||
}
|
||||
if (await file!.exists()) {
|
||||
String data = await file.readAsString();
|
||||
parser.defaultPolylineColor = Colors.orangeAccent;
|
||||
parser.defaultPolylineStroke = 5.0;
|
||||
parser.parseGeoJsonAsString(data);
|
||||
pathLayer = parser.polylines;
|
||||
}
|
||||
}
|
||||
|
||||
void pointsToMeasureSelectedValueChanged(int value) {
|
||||
pointsToMeasureSelectedValue.value = value;
|
||||
print('Selected point -> ${pointsToMeasureSelectedValue.value}');
|
||||
@@ -964,4 +1066,24 @@ class NavigationViewController extends GetxController {
|
||||
void addMeasuredPoint() {
|
||||
// _measuredPoints.add({"id": 4001, "latitude": 46.3455, "longitude": 19.652});
|
||||
}
|
||||
void saveGpsAddress(String address) {
|
||||
prefs.setString('gpsAddress', address);
|
||||
}
|
||||
|
||||
void saveGpsName(String name) {
|
||||
prefs.setString('gpsName', name);
|
||||
}
|
||||
|
||||
void saveNtripUserName(String username) {
|
||||
prefs.setString('ntripUserName', username);
|
||||
}
|
||||
|
||||
void saveNtripPassword(String password) {
|
||||
prefs.setString('ntripPassword', password);
|
||||
}
|
||||
|
||||
void saveVehicleNumber(int vehicleNumber) {
|
||||
prefs.setInt('vehicleNumber', vehicleNumber);
|
||||
print("Save vehicleNumber to prefs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ class NavigationView extends GetView<NavigationViewController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Navigáció'),
|
||||
Text(
|
||||
"Mérés: Budapest-4",
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
)
|
||||
// Text(
|
||||
// "Mérés: Zalaegerszeg",
|
||||
// style: TextStyle(fontSize: 12.0),
|
||||
// )
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -84,6 +84,16 @@ class NavigationView extends GetView<NavigationViewController> {
|
||||
const Icon(Icons.file_open, size: 26.0, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.ReadPathFromFile();
|
||||
},
|
||||
child: const Icon(Icons.line_weight_rounded,
|
||||
size: 26.0, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: GestureDetector(
|
||||
@@ -222,10 +232,11 @@ class NavigationView extends GetView<NavigationViewController> {
|
||||
? FlutterMap(
|
||||
mapController: controller.mapController,
|
||||
options: MapOptions(
|
||||
center: LatLng(controller.currentLatitude.value,
|
||||
initialCenter: LatLng(
|
||||
controller.currentLatitude.value,
|
||||
controller.currentLongitude.value),
|
||||
maxZoom: 25,
|
||||
zoom: controller.currentZoom.value,
|
||||
initialZoom: controller.currentZoom.value,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -241,13 +252,14 @@ class NavigationView extends GetView<NavigationViewController> {
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: controller.currentLocationMarker),
|
||||
MarkerLayer(markers: controller.parser.markers),
|
||||
// MarkerLayer(markers: controller.parser.markers),
|
||||
PolylineLayer(polylines: controller.pathLayer),
|
||||
MarkerLayer(
|
||||
markers: controller.pointsToMeasureMarker),
|
||||
// PolylineLayer(
|
||||
// polylines: controller.parser.polylines),
|
||||
PolylineLayer(
|
||||
polylines: controller.parser.polylines),
|
||||
PolyWidgetLayer(
|
||||
polyWidgets: controller.pointsToMeasureLabel)
|
||||
polyWidgets: controller.pointsToMeasureLabel),
|
||||
],
|
||||
)
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/controllers/map_controller.dart';
|
||||
import 'package:terepi_seged/pages/navigation/presentation/controllers/navigation_controller.dart';
|
||||
|
||||
class SettingsDialog extends StatelessWidget {
|
||||
final controller = Get.find<NavigationViewController>();
|
||||
SettingsDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
@@ -19,8 +19,24 @@ class SettingsDialog extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.vehicleNumber =
|
||||
int.parse(controller.vehicleNumberController.text);
|
||||
if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
controller.ntripUserName.value =
|
||||
controller.ntripUsernameController.text;
|
||||
controller.saveNtripUserName(
|
||||
controller.ntripUsernameController.text);
|
||||
}
|
||||
if (controller.ntripPasswordController.text.isNotEmpty) {
|
||||
controller.ntripPassword.value =
|
||||
controller.ntripPasswordController.text;
|
||||
controller.saveNtripPassword(
|
||||
controller.ntripPasswordController.text);
|
||||
}
|
||||
if (controller.vehicleNumberController.text.isNotEmpty) {
|
||||
controller.vehicleNumber.value =
|
||||
int.parse(controller.vehicleNumberController.text);
|
||||
controller.saveVehicleNumber(
|
||||
int.parse(controller.vehicleNumberController.text));
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
@@ -28,7 +44,29 @@ class SettingsDialog extends StatelessWidget {
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateProperty.all(Colors.transparent)),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
if (controller.ntripUsernameController.text.isNotEmpty) {
|
||||
controller.ntripUserName.value =
|
||||
controller.ntripUsernameController.text;
|
||||
controller.saveNtripUserName(
|
||||
controller.ntripUsernameController.text);
|
||||
}
|
||||
if (controller.ntripPasswordController.text.isNotEmpty) {
|
||||
controller.ntripPassword.value =
|
||||
controller.ntripPasswordController.text;
|
||||
controller.saveNtripPassword(
|
||||
controller.ntripPasswordController.text);
|
||||
}
|
||||
print(
|
||||
"vehicleNumberController: ${controller.vehicleNumberController.text}");
|
||||
if (controller.vehicleNumberController.text.isNotEmpty) {
|
||||
controller.vehicleNumber.value =
|
||||
int.parse(controller.vehicleNumberController.text);
|
||||
controller.saveVehicleNumber(
|
||||
int.parse(controller.vehicleNumberController.text));
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
child: const Text(
|
||||
'Bezár',
|
||||
style: TextStyle(color: Colors.blue, fontSize: 14.0),
|
||||
@@ -51,12 +89,95 @@ class SettingsDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Obx(() => Column(children: [
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-BE6A'),
|
||||
value: '10:06:1C:97:BE:6A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-BE6A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-BE6A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-1DC6'),
|
||||
value: 'E8:31:CD:16:1D:C6',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-1DC6';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-1DC6');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-9C3A'),
|
||||
value: '08:3A:8D:14:9C:3A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-9C3A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-9C3A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-72C2'),
|
||||
value: '10:06:1C:97:72:C2',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-72C2';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-72C2');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FE16'),
|
||||
value: '10:06:1C:9F:FE:16',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FE16';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FE16');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-3B0A'),
|
||||
value: '10:C6:1C:9E:3B:0A',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-3B0A';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-3B0A');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-7FEA'),
|
||||
value: '10:06:1C:9C:7F:EA',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-7FEA';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-7FEA');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-A39E'),
|
||||
value: '10:06:1C:97:A3:9E',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-A39E';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-A39E');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FF4E'),
|
||||
value: '98:CD:AC:62:FF:4E',
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FF4E';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FF4E');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-8BB2'),
|
||||
@@ -64,7 +185,9 @@ class SettingsDialog extends StatelessWidget {
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
;
|
||||
controller.gpsName.value = 'TiGNSS Rover-8BB2';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-8BB2');
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('TiGNSS Rover-FF36'),
|
||||
@@ -72,6 +195,9 @@ class SettingsDialog extends StatelessWidget {
|
||||
groupValue: controller.gpsAddress.value,
|
||||
onChanged: (value) {
|
||||
controller.gpsAddress.value = value!;
|
||||
controller.gpsName.value = 'TiGNSS Rover-FF36';
|
||||
controller.saveGpsAddress(value);
|
||||
controller.saveGpsName('TiGNSS Rover-FF36');
|
||||
})
|
||||
])),
|
||||
const Padding(
|
||||
@@ -173,15 +299,7 @@ class SettingsDialog extends StatelessWidget {
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 5),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(
|
||||
height: 5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class NtripSettingsController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
final hostController = TextEditingController();
|
||||
final portController = TextEditingController();
|
||||
final mountPointController = TextEditingController();
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
final isPasswordVisible = false.obs;
|
||||
final autoConnect = false.obs;
|
||||
final isBusy = false.obs;
|
||||
|
||||
NtripService get _service => NtripService.to;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
hostController.text = _service.host.value;
|
||||
portController.text = _service.port.value.toString();
|
||||
mountPointController.text = _service.mountpoint.value;
|
||||
usernameController.text = _service.username.value;
|
||||
autoConnect.value = _service.autoConnect.value;
|
||||
passwordController.text = _service.password.value;
|
||||
}
|
||||
|
||||
Future<bool> _validateAndSave() async {
|
||||
if (!(formKey.currentState?.validate() ?? false)) return false;
|
||||
|
||||
await _service.updateSettings(
|
||||
host: hostController.text,
|
||||
port: int.parse(portController.text.trim()),
|
||||
mountpoint: mountPointController.text,
|
||||
username: usernameController.text,
|
||||
password: passwordController.text,
|
||||
autoConnect: autoConnect.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> saveSettings() async {
|
||||
if (!await _validateAndSave()) return;
|
||||
|
||||
Get.back();
|
||||
Get.snackbar('NTRIP', 'Beállítások elmentve.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
|
||||
Future<void> saveAndConnect() async {
|
||||
try {
|
||||
isBusy.value = true;
|
||||
|
||||
if (!await _validateAndSave()) return;
|
||||
|
||||
// Ha már kapcsolódva vagyunk, előbb bontunk, hogy az új
|
||||
// beállításokkal épüljön újra a kapcsolat.
|
||||
if (_service.isConnected.value) {
|
||||
await _service.disconnect();
|
||||
}
|
||||
|
||||
await _service.connect();
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'NTRIP',
|
||||
'Kapcsolódva: ${_service.mountpoint.value}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFF2E7D32),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
);
|
||||
} on NtripException catch (e) {
|
||||
Get.snackbar(
|
||||
'NTRIP hiba',
|
||||
e.message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'NTRIP hiba',
|
||||
e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
);
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
try {
|
||||
isBusy.value = true;
|
||||
await _service.disconnect();
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
hostController.dispose();
|
||||
portController.dispose();
|
||||
mountPointController.dispose();
|
||||
usernameController.dispose();
|
||||
passwordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// „Kapcsolat tesztelése" — vevő nélkül is fut: mentés után csatlakozik,
|
||||
/// validálja a caster válaszát, majd azonnal bont.
|
||||
Future<void> testConnection() async {
|
||||
try {
|
||||
isBusy.value = true;
|
||||
if (!await _validateAndSave()) return;
|
||||
|
||||
final message = await _service.testConnection();
|
||||
Get.snackbar(
|
||||
'NTRIP teszt sikeres',
|
||||
message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFF2E7D32),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
} on NtripException catch (e) {
|
||||
Get.snackbar(
|
||||
'NTRIP teszt sikertelen',
|
||||
e.message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'NTRIP teszt sikertelen',
|
||||
e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
colorText: const Color(0xFFFFFFFF),
|
||||
);
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
|
||||
import '../controllers/ntrip_settings_controller.dart';
|
||||
|
||||
class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
||||
const NtripSettingsSheet({super.key});
|
||||
|
||||
static Future<void> show() async {
|
||||
Get.put(NtripSettingsController());
|
||||
try {
|
||||
await Get.bottomSheet(
|
||||
const NtripSettingsSheet(),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} finally {
|
||||
Get.delete<NtripSettingsController>();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: screenHeight * 0.86,
|
||||
),
|
||||
child: Material(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(24),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
reverse: true,
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 20 + bottomInset),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 42,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.settings_input_antenna),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'NTRIP beállítások',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: controller.hostController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Caster / host',
|
||||
hintText: 'pl. caster.example.com',
|
||||
prefixIcon: Icon(Icons.dns),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'A host megadása kötelező.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: controller.portController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Port',
|
||||
hintText: '2101',
|
||||
prefixIcon: Icon(Icons.tag),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
final port = int.tryParse(value?.trim() ?? '');
|
||||
if (port == null || port <= 0 || port > 65535) {
|
||||
return 'Érvényes portszámot adj meg.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: controller.mountPointController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mountpoint',
|
||||
hintText: 'pl. SGO_RTK3.2',
|
||||
prefixIcon: Icon(Icons.place),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'A mountpoint megadása kötelező.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: controller.usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Felhasználónév',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Obx(() {
|
||||
return TextFormField(
|
||||
controller: controller.passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jelszó',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordVisible.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.isPasswordVisible.value =
|
||||
!controller.isPasswordVisible.value;
|
||||
},
|
||||
),
|
||||
),
|
||||
obscureText: !controller.isPasswordVisible.value,
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
Obx(() {
|
||||
return SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Automatikus kapcsolódás'),
|
||||
subtitle: const Text(
|
||||
'RTK képes vevő csatlakozásakor próbáljon automatikusan NTRIP-re kapcsolódni.',
|
||||
),
|
||||
value: controller.autoConnect.value,
|
||||
onChanged: (value) {
|
||||
controller.autoConnect.value = value;
|
||||
},
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
Obx(() {
|
||||
final isBusy = controller.isBusy.value;
|
||||
final isConnected = NtripService.to.isConnected.value;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (isConnected)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed:
|
||||
isBusy ? null : controller.disconnect,
|
||||
icon: const Icon(Icons.link_off),
|
||||
label: const Text('Bontás'),
|
||||
),
|
||||
),
|
||||
if (isConnected) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed:
|
||||
isBusy ? null : controller.saveAndConnect,
|
||||
icon: isBusy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.play_arrow),
|
||||
label: Text(
|
||||
isBusy
|
||||
? 'Kapcsolódás...'
|
||||
: 'Mentés és kapcsolódás',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
await controller.saveSettings();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Csak mentés'),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => OutlinedButton.icon(
|
||||
onPressed: controller.isBusy.value
|
||||
? null
|
||||
: controller.testConnection,
|
||||
icon: const Icon(Icons.network_check, size: 18),
|
||||
label: const Text(
|
||||
'Kapcsolat tesztelése (vevő nélkül)'))))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
import 'package:terepi_seged/widgets/gnss_device_picker_dialog.dart';
|
||||
|
||||
/// Központi beállítások oldal.
|
||||
///
|
||||
/// Material 3 konvenciók:
|
||||
/// - egyetlen görgethető lista, szekciókra bontva
|
||||
/// - az aktuális érték a subtitle-ben látszik
|
||||
/// - kapcsolók azonnal érvényesülnek (nincs "Mentés" gomb ezen a szinten)
|
||||
/// - űrlap-jellegű beállítás (NTRIP) al-sheetre megy, explicit mentéssel
|
||||
///
|
||||
/// Bővítés: új ListTile / SwitchListTile a megfelelő szekcióba,
|
||||
/// vagy új _SettingsSection, ha új témakör jön.
|
||||
class SettingsView extends StatelessWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Beállítások'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
// ── Kapcsolat ─────────────────────────────────────────────
|
||||
const _SettingsSection('Kapcsolat'),
|
||||
|
||||
// NTRIP — subtitle mutatja az aktuális konfigurációt,
|
||||
// a trailing pötty a kapcsolat állapotát.
|
||||
Obx(() {
|
||||
final ntrip = NtripService.to;
|
||||
final configured = ntrip.hasCompleteSettings;
|
||||
final subtitle = configured
|
||||
? '${ntrip.host.value} · ${ntrip.mountpoint.value}'
|
||||
: 'Nincs beállítva';
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.cell_tower),
|
||||
title: const Text('NTRIP korrekció'),
|
||||
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
|
||||
trailing: Icon(
|
||||
Icons.circle,
|
||||
size: 12,
|
||||
color: ntrip.isConnected.value
|
||||
? Colors.green
|
||||
: (configured ? Colors.grey : Colors.orange),
|
||||
),
|
||||
onTap: NtripSettingsSheet.show,
|
||||
);
|
||||
}),
|
||||
|
||||
// GNSS eszköz — ugyanaz a választó, mint a drawer-ben.
|
||||
Obx(() {
|
||||
final device = GnssDeviceService.to.selectedDevice.value;
|
||||
final connected = GnssService.to.connectionState.value ==
|
||||
GnssConnectionState.connected;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
device?.type == GnssConnectionType.ble
|
||||
? Icons.bluetooth_searching
|
||||
: device?.type == GnssConnectionType.phoneGps
|
||||
? Icons.phone_android
|
||||
: Icons.bluetooth,
|
||||
),
|
||||
title: const Text('GNSS eszköz'),
|
||||
subtitle: Text(
|
||||
device?.name ?? 'Nincs kiválasztva',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.circle,
|
||||
size: 12,
|
||||
color: connected ? Colors.green : Colors.grey,
|
||||
),
|
||||
onTap: GnssDevicePickerDialog.show,
|
||||
);
|
||||
}),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// ── Térkép ────────────────────────────────────────────────
|
||||
const _SettingsSection('Térkép'),
|
||||
|
||||
// Helykitöltők — a onTap kitöltésével élesíthetők,
|
||||
// az enabled: false vizuálisan is jelzi, hogy még nem aktívak.
|
||||
const ListTile(
|
||||
leading: Icon(Icons.map_outlined),
|
||||
title: Text('Alaptérkép'),
|
||||
subtitle: Text('OpenStreetMap', style: TextStyle(fontSize: 12)),
|
||||
enabled: false,
|
||||
),
|
||||
const ListTile(
|
||||
leading: Icon(Icons.straighten),
|
||||
title: Text('Koordináta-megjelenítés'),
|
||||
subtitle: Text('EOV', style: TextStyle(fontSize: 12)),
|
||||
enabled: false,
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// ── Névjegy ───────────────────────────────────────────────
|
||||
const _SettingsSection('Névjegy'),
|
||||
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (_, snap) => ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Verzió'),
|
||||
subtitle: Text(
|
||||
snap.hasData
|
||||
? '${snap.data!.version} (build ${snap.data!.buildNumber})'
|
||||
: '…',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: const Text('Nyílt forráskódú licencek'),
|
||||
onTap: () => showLicensePage(
|
||||
context: context,
|
||||
applicationName: 'Terepi Segéd',
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Szekció felirat — ugyanaz a stílus, mint a drawer-ben ───────────
|
||||
|
||||
class _SettingsSection extends StatelessWidget {
|
||||
final String text;
|
||||
const _SettingsSection(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Text(
|
||||
text.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade500,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/home/presentation/controllers/home_controller.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/controllers/map_controller.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
|
||||
import '../presentations/controllers/shell_controller.dart';
|
||||
|
||||
class ShellBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// TODO: implement dependencies
|
||||
Get.put(ShellController());
|
||||
Get.put(HomeViewController());
|
||||
// Get.put(MapViewController());
|
||||
// Get.put(MapSurveyController());
|
||||
Get.lazyPut<MapSurveyController>(
|
||||
() => MapSurveyController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ShellController extends GetxController {
|
||||
static ShellController get to => Get.find();
|
||||
|
||||
final currentIndex = 1.obs;
|
||||
final isNavBarVisible = true.obs;
|
||||
|
||||
static const titles = [
|
||||
'Térkép',
|
||||
'Pontmérés',
|
||||
'Nyomvonal',
|
||||
'Adatok',
|
||||
];
|
||||
|
||||
String get currentTitle => titles[currentIndex.value];
|
||||
|
||||
void goToTab(int index) {
|
||||
if (index < 0 || index >= titles.length) return;
|
||||
currentIndex.value = index;
|
||||
}
|
||||
|
||||
// Shortcutok — a Drawerből hívhatók
|
||||
void goToMap() => goToTab(0);
|
||||
void goToSurvey() => goToTab(1);
|
||||
void goToTracking() => goToTab(2);
|
||||
void goToData() => goToTab(3);
|
||||
|
||||
void showNavBar() => isNavBarVisible.value = true;
|
||||
void hideNavBar() => isNavBarVisible.value = false;
|
||||
void toggleNavBar() => isNavBarVisible.value = !isNavBarVisible.value;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/home/presentation/views/home_view.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||
import 'package:terepi_seged/widgets/gnss_status_chip.dart';
|
||||
import 'package:terepi_seged/widgets/shell_adaptive_appbar.dart';
|
||||
import 'package:terepi_seged/widgets/appbar/shell_map_appbar.dart';
|
||||
|
||||
import '../../../../widgets/app_drawer.dart';
|
||||
import '../../../map_survey/presentations/views/map_survey_view.dart';
|
||||
import '../controllers/shell_controller.dart';
|
||||
|
||||
class ShellView extends GetView<ShellController> {
|
||||
const ShellView({super.key});
|
||||
|
||||
static const _pages = [
|
||||
HomeView(),
|
||||
//MapView(),
|
||||
MapSurveyView(),
|
||||
//TrackingView(),
|
||||
//DataView(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapController = Get.find<MapSurveyController>();
|
||||
|
||||
return Scaffold(
|
||||
// extendBody: true,
|
||||
// extendBodyBehindAppBar: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: ShellMapAppBar(controller: mapController),
|
||||
// appBar: AppBar(
|
||||
// // Cím reaktívan frissül tab váltáskor
|
||||
// title: Obx(() => Text(controller.currentTitle)),
|
||||
// actions: [
|
||||
// const GnssStatusChip(),
|
||||
// const SizedBox(width: 6),
|
||||
// Obx(() => controller.currentIndex.value == 1
|
||||
// ? NtripStatusChip(
|
||||
// isConnected: NtripService.to.isConnected,
|
||||
// onToggle: () {
|
||||
// NtripService.to.isConnected.value
|
||||
// ? NtripService.to.disconnect()
|
||||
// : NtripService.to.connect();
|
||||
// },
|
||||
// )
|
||||
// : const SizedBox.shrink())
|
||||
// ],
|
||||
// ),
|
||||
drawer: const AppDrawer(),
|
||||
body: Obx(() => IndexedStack(
|
||||
index: controller.currentIndex.value,
|
||||
children: _pages,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ class SocketTestController extends GetxController {
|
||||
StreamSubscription? socketStreamSubscription;
|
||||
RxString message = ''.obs;
|
||||
|
||||
final GoogleSignIn googleSignIn =
|
||||
GoogleSignIn(scopes: ['https://www.googleapis.com/auth/drive.appdata']);
|
||||
late GoogleSignInAccount? googleSignInAccount;
|
||||
// final GoogleSignIn googleSignIn =
|
||||
// GoogleSignIn(scopes: ['https://www.googleapis.com/auth/drive.appdata']);
|
||||
// late GoogleSignInAccount? googleSignInAccount;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -31,13 +31,13 @@ class SocketTestController extends GetxController {
|
||||
}
|
||||
|
||||
void googleLogIn() async {
|
||||
googleSignInAccount = await googleSignIn.signIn();
|
||||
// googleSignInAccount = await googleSignIn.signIn();
|
||||
|
||||
final GoogleSignInAuthentication googleSignInAuthentication =
|
||||
await googleSignInAccount!.authentication;
|
||||
// final GoogleSignInAuthentication googleSignInAuthentication =
|
||||
// await googleSignInAccount!.authentication;
|
||||
|
||||
print("AccessToken: ${googleSignInAuthentication.accessToken}");
|
||||
print("IdToken: ${googleSignInAuthentication.idToken}");
|
||||
// print("AccessToken: ${googleSignInAuthentication.accessToken}");
|
||||
// print("IdToken: ${googleSignInAuthentication.idToken}");
|
||||
}
|
||||
|
||||
void connect() async {
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:terepi_seged/models/stakeout_point.dart';
|
||||
import 'package:terepi_seged/services/app_database.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_import_service.dart';
|
||||
import 'package:terepi_seged/services/stakeout_service.dart';
|
||||
|
||||
/// Kitűzési pontok importja (CSV / GeoJSON) — felismerés + előnézet.
|
||||
///
|
||||
/// Folyamat: fájlválasztás → automatikus elemzés (elválasztó, tizedesjel,
|
||||
/// fejléc, oszlopszerepek, koordináta-rendszer) → ELŐNÉZET: minta-táblázat
|
||||
/// oszloponkénti szerep-választóval + mini-térkép vizuális ellenőrzéshez →
|
||||
/// import az aktív projektbe. Soha nem importálunk vakon.
|
||||
class StakeoutImportView extends StatefulWidget {
|
||||
const StakeoutImportView({super.key});
|
||||
|
||||
@override
|
||||
State<StakeoutImportView> createState() => _StakeoutImportViewState();
|
||||
}
|
||||
|
||||
class _StakeoutImportViewState extends State<StakeoutImportView> {
|
||||
CsvPreview? _preview;
|
||||
List<ColumnRole> _roles = [];
|
||||
String? _error;
|
||||
bool _busy = false;
|
||||
|
||||
Future<void> _pickFile() async {
|
||||
setState(() {
|
||||
_error = null;
|
||||
_busy = true;
|
||||
});
|
||||
try {
|
||||
// FileType.any: a Google Drive (és más felhő-providerek) a tárolt
|
||||
// MIME-típus alapján szűrnek — a .geojson-nak nincs regisztrált
|
||||
// MIME-je, a Drive a csv/txt fájlokat is gyakran más MIME-mal
|
||||
// tartja nyilván, ezért custom szűrővel szürkék maradnának.
|
||||
// A kiterjesztést a kiválasztás UTÁN mi ellenőrizzük.
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
final picked = result?.files.single;
|
||||
final path = picked?.path;
|
||||
if (path == null) return;
|
||||
|
||||
final name = picked!.name.toLowerCase();
|
||||
const allowed = ['.csv', '.txt', '.geojson', '.json'];
|
||||
if (!allowed.any(name.endsWith)) {
|
||||
setState(() => _error = 'Nem támogatott fájltípus: ${picked.name} — '
|
||||
'CSV, TXT vagy GeoJSON fájlt válassz.');
|
||||
return;
|
||||
}
|
||||
|
||||
final file = File(path);
|
||||
final isGeojson = name.endsWith('.json') || name.endsWith('.geojson');
|
||||
final preview = isGeojson
|
||||
? await StakeoutImportService.analyzeGeojson(file)
|
||||
: await StakeoutImportService.analyzeCsv(file);
|
||||
|
||||
setState(() {
|
||||
_preview = preview;
|
||||
_roles = List.of(preview.guessedRoles);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
} finally {
|
||||
setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Az aktuális szerep-kiosztással felépített pontok (a mini-térképhez
|
||||
/// és az importhoz ugyanaz a kód fut — amit látsz, azt kapod).
|
||||
({List<StakeoutPoint> points, int skipped})? _build() {
|
||||
final preview = _preview;
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
if (preview == null || projectId == null) return null;
|
||||
if (!Get.isRegistered<CoordConverterService>()) return null;
|
||||
try {
|
||||
return StakeoutImportService.buildPoints(
|
||||
preview: preview,
|
||||
roles: _roles,
|
||||
projectId: projectId,
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _import() async {
|
||||
final built = _build();
|
||||
if (built == null || built.points.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Import',
|
||||
'Nincs importálható pont — ellenőrizd az '
|
||||
'oszlop-megfeleltetést.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
return;
|
||||
}
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
final inserted =
|
||||
await AppDatabase.instance.insertStakeoutPoints(built.points);
|
||||
final dup = built.points.length - inserted;
|
||||
if (Get.isRegistered<StakeoutService>()) {
|
||||
await StakeoutService.to.load();
|
||||
}
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Import kész',
|
||||
'$inserted pont importálva'
|
||||
'${dup > 0 ? ' · $dup már létező kihagyva' : ''}'
|
||||
'${built.skipped > 0 ? ' · ${built.skipped} hibás sor' : ''}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final preview = _preview;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Kitűzési pontok importja')),
|
||||
body: _busy && preview == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: preview == null
|
||||
? _EmptyState(onPick: _pickFile, error: _error)
|
||||
: _buildPreview(context, preview),
|
||||
bottomNavigationBar: preview == null
|
||||
? null
|
||||
: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: _busy ? null : _pickFile,
|
||||
child: const Text('Másik fájl'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: _busy ? null : _import,
|
||||
icon: _busy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child:
|
||||
CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.download_done),
|
||||
label: const Text('Import az aktív projektbe'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreview(BuildContext context, CsvPreview preview) {
|
||||
final built = _build();
|
||||
final crsLabel =
|
||||
_roles.contains(ColumnRole.eovY) && _roles.contains(ColumnRole.eovX)
|
||||
? 'EOV'
|
||||
: _roles.contains(ColumnRole.lat) && _roles.contains(ColumnRole.lon)
|
||||
? 'WGS84'
|
||||
: 'nincs koordináta kijelölve!';
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// ── Összegző chipek ─────────────────────────────────────────
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
Chip(
|
||||
avatar: const Icon(Icons.description, size: 16),
|
||||
label:
|
||||
Text(preview.fileName, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
Chip(
|
||||
label: Text('${preview.rowCount} sor',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
Chip(
|
||||
avatar: Icon(
|
||||
crsLabel.startsWith('nincs')
|
||||
? Icons.warning_amber
|
||||
: Icons.public,
|
||||
size: 16,
|
||||
color: crsLabel.startsWith('nincs') ? Colors.orange : null,
|
||||
),
|
||||
label: Text('Rendszer: $crsLabel',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
if (built != null)
|
||||
Chip(
|
||||
label: Text(
|
||||
'${built.points.length} érvényes pont'
|
||||
'${built.skipped > 0 ? ' · ${built.skipped} hibás sor' : ''}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Megfeleltetési táblázat ────────────────────────────────
|
||||
Text('Oszlop-megfeleltetés',
|
||||
style: Theme.of(context).textTheme.titleSmall),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Ellenőrizd a felismert szerepeket — az oszlopok fölött '
|
||||
'módosíthatók.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
headingRowHeight: 96,
|
||||
columnSpacing: 16,
|
||||
columns: [
|
||||
for (var c = 0; c < preview.headers.length; c++)
|
||||
DataColumn(
|
||||
label: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(preview.headers[c],
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.grey)),
|
||||
DropdownButton<ColumnRole>(
|
||||
value: _roles[c],
|
||||
isDense: true,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: _roles[c] == ColumnRole.ignore
|
||||
? FontWeight.normal
|
||||
: FontWeight.w600,
|
||||
color: _roles[c] == ColumnRole.ignore
|
||||
? Colors.grey
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
items: [
|
||||
for (final r in ColumnRole.values)
|
||||
DropdownMenuItem(value: r, child: Text(r.label)),
|
||||
],
|
||||
onChanged: (r) {
|
||||
if (r == null) return;
|
||||
setState(() {
|
||||
// Egy szerep csak egy oszlopé lehet.
|
||||
if (r != ColumnRole.ignore) {
|
||||
for (var i = 0; i < _roles.length; i++) {
|
||||
if (_roles[i] == r) {
|
||||
_roles[i] = ColumnRole.ignore;
|
||||
}
|
||||
}
|
||||
}
|
||||
_roles[c] = r;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: [
|
||||
for (final row in preview.sampleRows)
|
||||
DataRow(cells: [
|
||||
for (var c = 0; c < preview.headers.length; c++)
|
||||
DataCell(Text(
|
||||
c < row.length ? row[c] : '',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
)),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Mini-térkép: vizuális ellenőrzés ───────────────────────
|
||||
if (built != null && built.points.isNotEmpty) ...[
|
||||
Text('Előnézet a térképen',
|
||||
style: Theme.of(context).textTheme.titleSmall),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Ha a pontok ott vannak, ahol lenniük kell, a megfeleltetés jó.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: _PreviewMap(points: built.points),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewMap extends StatelessWidget {
|
||||
final List<StakeoutPoint> points;
|
||||
const _PreviewMap({required this.points});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Max. 500 markert rajzolunk — előnézetnek bőven elég.
|
||||
final shown = points.length > 500
|
||||
? [
|
||||
for (var i = 0; i < points.length; i += points.length ~/ 500)
|
||||
points[i]
|
||||
]
|
||||
: points;
|
||||
|
||||
final lats = shown.map((p) => p.planLat);
|
||||
final lons = shown.map((p) => p.planLon);
|
||||
final center = LatLng(
|
||||
(lats.reduce((a, b) => a + b)) / shown.length,
|
||||
(lons.reduce((a, b) => a + b)) / shown.length,
|
||||
);
|
||||
|
||||
return FlutterMap(
|
||||
options: MapOptions(initialCenter: center, initialZoom: 13),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'hu.appdev.terepi_seged',
|
||||
),
|
||||
MarkerLayer(markers: [
|
||||
for (final p in shown)
|
||||
Marker(
|
||||
point: LatLng(p.planLat, p.planLon),
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepOrange,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
final VoidCallback onPick;
|
||||
final String? error;
|
||||
const _EmptyState({required this.onPick, this.error});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.upload_file, size: 56, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Válassz CSV vagy GeoJSON fájlt.\n'
|
||||
'A program felismeri az elválasztót, a tizedesjelet és az '
|
||||
'oszlopok szerepét (EOV / WGS84), import előtt pedig '
|
||||
'ellenőrizheted az eredményt.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
if (error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text('Hiba: $error',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.red)),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: onPick,
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: const Text('Fájl kiválasztása'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ class StartPageController extends GetxController {
|
||||
|
||||
void _departStartPage() async {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
Get.offAllNamed(Routes.HOME);
|
||||
//Get.offAllNamed(Routes.HOME);
|
||||
Get.offAllNamed(Routes.SHELL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||
|
||||
class TrackingBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
if (!Get.isRegistered<TrackingController>()) {
|
||||
Get.put(TrackingController(), permanent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
import 'dart:async';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:terepi_seged/services/app_logger.dart';
|
||||
import 'package:terepi_seged/services/firebase_logger.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:terepi_seged/services/track_sync_service.dart';
|
||||
|
||||
import '../../../../services/location_source.dart';
|
||||
import '../../../../services/phone_gps_source.dart';
|
||||
import '../../../../services/track_database.dart';
|
||||
import '../../../../services/gpx_exporter.dart';
|
||||
import '../../../../models/track.dart';
|
||||
|
||||
// ─── Foreground task handler ─────────────────────────────────────────────────
|
||||
// Ez a függvény a háttér-isolate-ban fut. Csak a meglevő pozíció-streamet
|
||||
// tartja fenn, a tényleges adatfeldolgozás a főszálban történik.
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void startTrackingCallback() {
|
||||
FlutterForegroundTask.setTaskHandler(_TrackingTaskHandler());
|
||||
}
|
||||
|
||||
class _TrackingTaskHandler extends TaskHandler {
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
|
||||
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp) {
|
||||
// A Geolocator stream a főizoláton folyik tovább, itt nincs teendő.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
|
||||
}
|
||||
|
||||
// ─── TrackingController ──────────────────────────────────────────────────────
|
||||
|
||||
class TrackingController extends GetxController {
|
||||
static TrackingController get to => Get.find();
|
||||
|
||||
// ── Állapot ────────────────────────────────────────────────────────────────
|
||||
final isRecording = false.obs;
|
||||
final isPaused = false.obs;
|
||||
final currentTrack = Rxn<Track>();
|
||||
|
||||
/// Aktuális session pontjai a térképhez (csak a memóriában).
|
||||
final livePoints = <LatLng>[].obs;
|
||||
|
||||
/// Aktuális sebesség [km/h].
|
||||
final currentSpeedKmh = 0.0.obs;
|
||||
|
||||
/// Megtett távolság [m] az aktuális sessionben.
|
||||
final sessionDistance = 0.0.obs;
|
||||
|
||||
/// Eltelt idő formátuma óó:pp:mm.
|
||||
final elapsedFormatted = '00:00:00'.obs;
|
||||
|
||||
/// Előtér/háttér állapot jelzése.
|
||||
final isInBackground = false.obs;
|
||||
|
||||
// ── Mentett track-ek listája ────────────────────────────────────────────────
|
||||
final savedTracks = <Track>[].obs;
|
||||
|
||||
// Melyik track-ek látszanak overlay-ként a térképen
|
||||
final overlayTrackIds = <int>[].obs;
|
||||
|
||||
// Betöltött koordináták cache
|
||||
final Map<int, List<LatLng>> _trackCoords = {};
|
||||
|
||||
int get livePointCount => livePoints.length;
|
||||
|
||||
// ── Belső állapot ──────────────────────────────────────────────────────────
|
||||
LocationSource? _source;
|
||||
StreamSubscription<SourcePosition>? _positionSub;
|
||||
Timer? _elapsedTimer;
|
||||
|
||||
TrackPoint? _lastPoint;
|
||||
DateTime? _sessionStart;
|
||||
double _accumulatedDistance = 0;
|
||||
|
||||
final _db = TrackDatabase.instance;
|
||||
final _exporter = GpxExporter();
|
||||
|
||||
// Timer
|
||||
DateTime? _lastPositionTime;
|
||||
Timer? _watchdogTimer;
|
||||
final gpsSignalLost = false.obs;
|
||||
|
||||
static const _gpsTimeoutSec = 10;
|
||||
|
||||
// ── Inicializálás ──────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initForegroundTask();
|
||||
loadSavedTracks();
|
||||
|
||||
// A TrackingController permanent, egyszer indul az app életciklusában —
|
||||
// enélkül projektváltáskor a lista a réginél ragadna.
|
||||
ever(ProjectService.to.activeProject, (_) {
|
||||
loadSavedTracks();
|
||||
// Ne maradjon a térképen egy MÁSIK projekt track-overlaya.
|
||||
|
||||
overlayTrackIds.clear();
|
||||
_trackCoords.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _initForegroundTask() {
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'tracking_channel',
|
||||
channelName: 'Nyomvonal rögzítés',
|
||||
channelDescription: 'Aktív track rögzítése folyamatban',
|
||||
channelImportance: NotificationChannelImportance.LOW,
|
||||
priority: NotificationPriority.LOW,
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: true,
|
||||
playSound: false,
|
||||
),
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
eventAction: ForegroundTaskEventAction.repeat(1000),
|
||||
autoRunOnBoot: false,
|
||||
allowWakeLock: true,
|
||||
allowWifiLock: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Publikus API ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Elindítja a rögzítést a megadott forrással (alapértelmezett: telefon GPS).
|
||||
Future<void> startRecording(
|
||||
{LocationSource? source, String? name, TrackSyncMode? syncMode}) async {
|
||||
if (isRecording.value) return;
|
||||
|
||||
// Alapértelmezés: a track kövesse az AKTÍV PROJEKT online/lokális
|
||||
// állapotát — eddig ez mindig 'online' volt, függetlenül a projekt
|
||||
// beállításától. Explicit paraméterrel felülírható.
|
||||
final activeProject = ProjectService.to.activeProject.value;
|
||||
final effectiveSyncMode = syncMode ??
|
||||
(activeProject?.isLocalOnly ?? true
|
||||
? TrackSyncMode.localOnly
|
||||
: TrackSyncMode.online);
|
||||
|
||||
_source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0);
|
||||
|
||||
// Track létrehozása az adatbázisban
|
||||
final now = DateTime.now();
|
||||
final trackName = name ?? DateFormat('yyyy-MM-dd HH:mm').format(now);
|
||||
final trackId = await _db.insertTrack(Track(
|
||||
name: trackName,
|
||||
startTime: now,
|
||||
source: _source!.displayName,
|
||||
projectId: activeProject?.id,
|
||||
syncMode: effectiveSyncMode));
|
||||
currentTrack.value = await _db.getTrack(trackId);
|
||||
|
||||
// Állapot reset
|
||||
livePoints.clear();
|
||||
sessionDistance.value = 0;
|
||||
_accumulatedDistance = 0;
|
||||
_lastPoint = null;
|
||||
_sessionStart = now;
|
||||
|
||||
// Foreground service indítása (háttér-működéshez)
|
||||
await FlutterForegroundTask.startService(
|
||||
notificationTitle: 'Track rögzítése',
|
||||
notificationText: trackName,
|
||||
callback: startTrackingCallback,
|
||||
);
|
||||
|
||||
// A TrackSyncService.startSession() maga dönti el (a track
|
||||
// isLocalOnly-ja alapján), hogy indítja-e a szinkron-időzítőket —
|
||||
// nincs többé külön "előbb hozzuk létre szerveroldalon" lépés, mert
|
||||
// az egységes (uuid-alapú) push maga hozza létre a sort a szerveren.
|
||||
TrackSyncService.to.startSession(currentTrack.value!);
|
||||
// GPS stream feliratkozás
|
||||
_positionSub = _source!.positionStream.listen(
|
||||
_onPosition,
|
||||
onError: (e) {
|
||||
AppLogger.e('positionStream', 'Stream hiba', error: e);
|
||||
FirebaseLogger.e('positionStream', 'Stream hiba', error: e);
|
||||
if (e is LocationServiceDisabledException ||
|
||||
e is PermissionDeniedException) {
|
||||
Get.snackbar('GPS hiba', e.toString(),
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
stopRecording();
|
||||
}
|
||||
},
|
||||
);
|
||||
AppLogger.e('track_started', 'name:$trackName');
|
||||
FirebaseLogger.event('track_started', {'name': trackName});
|
||||
|
||||
// Időmérő
|
||||
_startElapsedTimer();
|
||||
|
||||
isRecording.value = true;
|
||||
isPaused.value = false;
|
||||
_startWatchdog();
|
||||
}
|
||||
|
||||
void pauseRecording() {
|
||||
if (!isRecording.value || isPaused.value) return;
|
||||
_positionSub?.pause();
|
||||
_elapsedTimer?.cancel();
|
||||
isPaused.value = true;
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationTitle: 'Track szüneteltetve',
|
||||
notificationText: currentTrack.value?.name ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
void resumeRecording() {
|
||||
if (!isPaused.value) return;
|
||||
_positionSub?.resume();
|
||||
_startElapsedTimer();
|
||||
isPaused.value = false;
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationTitle: 'Track rögzítése',
|
||||
notificationText: currentTrack.value?.name ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stopRecording() async {
|
||||
if (!isRecording.value) return;
|
||||
|
||||
await _positionSub?.cancel();
|
||||
_positionSub = null;
|
||||
_elapsedTimer?.cancel();
|
||||
_elapsedTimer = null;
|
||||
|
||||
// Track lezárása az adatbázisban
|
||||
final track = currentTrack.value;
|
||||
if (track?.id != null) {
|
||||
final finished = track!.copyWith(
|
||||
status: TrackStatus.finished,
|
||||
endTime: DateTime.now(),
|
||||
distanceMeters: _accumulatedDistance,
|
||||
pointCount: livePoints.length,
|
||||
);
|
||||
await _db.updateTrack(finished);
|
||||
await TrackSyncService.to.stopSession(finished);
|
||||
currentTrack.value = finished;
|
||||
}
|
||||
|
||||
await FlutterForegroundTask.stopService();
|
||||
await _source?.dispose();
|
||||
_source = null;
|
||||
|
||||
isRecording.value = false;
|
||||
isPaused.value = false;
|
||||
|
||||
await loadSavedTracks();
|
||||
|
||||
_watchdogTimer?.cancel();
|
||||
_watchdogTimer = null;
|
||||
gpsSignalLost.value = false;
|
||||
}
|
||||
|
||||
Future<void> loadSavedTracks() async {
|
||||
savedTracks.value =
|
||||
await _db.listTracks(projectId: ProjectService.to.activeProjectId);
|
||||
}
|
||||
|
||||
Future<void> deleteTrack(int id) async {
|
||||
await _db.deleteTrack(id);
|
||||
overlayTrackIds.remove(id);
|
||||
_trackCoords.remove(id);
|
||||
await loadSavedTracks();
|
||||
}
|
||||
|
||||
/// GPX export + rendszer megosztás dialóg.
|
||||
Future<void> exportTrack(Track track) async {
|
||||
try {
|
||||
final path = await _exporter.export(track);
|
||||
await Share.shareXFiles([XFile(path)],
|
||||
subject: 'Nyomvonal: ${track.name}.gpx');
|
||||
} catch (e) {
|
||||
Get.snackbar('Export hiba', e.toString(),
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
}
|
||||
}
|
||||
|
||||
/// Visszatölt egy mentett track pontjait a térképre (megtekintés).
|
||||
Future<List<LatLng>> loadTrackPoints(int trackId) async {
|
||||
final pts = await _db.getLatLons(trackId);
|
||||
return pts.map((p) => LatLng(p.lat, p.lon)).toList();
|
||||
}
|
||||
|
||||
// ── Belső logika ───────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _onPosition(SourcePosition pos) async {
|
||||
try {
|
||||
if (isPaused.value) return;
|
||||
_lastPositionTime = DateTime.now();
|
||||
gpsSignalLost.value = false;
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
final trackId = currentTrack.value?.id;
|
||||
if (trackId == null) return;
|
||||
|
||||
final point = TrackPoint(
|
||||
trackId: trackId,
|
||||
latitude: pos.latitude,
|
||||
longitude: pos.longitude,
|
||||
altitude: pos.altitude,
|
||||
accuracy: pos.accuracy,
|
||||
speed: pos.speed,
|
||||
heading: pos.heading,
|
||||
timestamp: pos.timestamp,
|
||||
);
|
||||
|
||||
// Távolság a legutóbbi ponttól
|
||||
double segmentDist = 0;
|
||||
if (_lastPoint != null) {
|
||||
segmentDist = haversineMeters(
|
||||
_lastPoint!.latitude,
|
||||
_lastPoint!.longitude,
|
||||
pos.latitude,
|
||||
pos.longitude,
|
||||
);
|
||||
// Szűrés: ugrásszerű változás (pl. GPS lock elvesztése) ignorálása
|
||||
if (segmentDist > 100) {
|
||||
AppLogger.w(
|
||||
'_onPosition',
|
||||
'GPS ugrás kiszűrve: ${segmentDist.toStringAsFixed(0)}m '
|
||||
'(pts: ${livePoints.length})');
|
||||
_lastPoint = point; // reset — következő pont ettől mér
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_accumulatedDistance += segmentDist;
|
||||
sessionDistance.value = _accumulatedDistance;
|
||||
|
||||
// Sebesség km/h-ban
|
||||
currentSpeedKmh.value = (pos.speed ?? 0) * 3.6;
|
||||
|
||||
// Pont mentése
|
||||
await _db.addPoint(point, _accumulatedDistance);
|
||||
sw.stop();
|
||||
|
||||
if (sw.elapsedMilliseconds > 300) {
|
||||
AppLogger.w(
|
||||
'on_Position',
|
||||
'Lassú addPoint: ${sw.elapsedMilliseconds}ms, '
|
||||
'pts: ${livePoints.length}');
|
||||
FirebaseLogger.w(
|
||||
'on_Position',
|
||||
'Lassú addPoint: ${sw.elapsedMilliseconds}ms, '
|
||||
'pts: ${livePoints.length}');
|
||||
}
|
||||
|
||||
TrackSyncService.to.onNewPoint(point);
|
||||
|
||||
_lastPoint = point;
|
||||
|
||||
// UI frissítés
|
||||
livePoints.add(LatLng(pos.latitude, pos.longitude));
|
||||
|
||||
// Értesítés frissítése
|
||||
if (livePoints.length % 10 == 0) {
|
||||
final dist = _formatDistance(_accumulatedDistance);
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationTitle: 'Track rögzítése – $dist',
|
||||
notificationText: elapsedFormatted.value,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.e('_onPosition', 'pont feldolgozási hiba - track folytatódik',
|
||||
error: e, stack: stack);
|
||||
}
|
||||
}
|
||||
|
||||
void _startElapsedTimer() {
|
||||
_elapsedTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (_sessionStart == null) return;
|
||||
final d = DateTime.now().difference(_sessionStart!);
|
||||
final h = d.inHours.toString().padLeft(2, '0');
|
||||
final m = (d.inMinutes % 60).toString().padLeft(2, '0');
|
||||
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
|
||||
elapsedFormatted.value = '$h:$m:$s';
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDistance(double m) {
|
||||
if (m < 1000) return '${m.toStringAsFixed(0)} m';
|
||||
return '${(m / 1000).toStringAsFixed(2)} km';
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
stopRecording();
|
||||
if (!isRecording.value) {
|
||||
_positionSub?.cancel();
|
||||
_elapsedTimer?.cancel();
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Overlay ki/be kapcsolása — ha bekapcsol, betölti a koordinátákat
|
||||
void toggleTrackOverlay(int trackId) {
|
||||
if (overlayTrackIds.contains(trackId)) {
|
||||
overlayTrackIds.remove(trackId);
|
||||
overlayTrackIds.refresh();
|
||||
} else {
|
||||
overlayTrackIds.add(trackId);
|
||||
overlayTrackIds.refresh();
|
||||
_loadTrackCoords(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Koordináták visszaadása — üres lista ha még nincs betöltve
|
||||
List<LatLng> getCoordsFor(int trackId) => _trackCoords[trackId] ?? [];
|
||||
|
||||
Future<void> _loadTrackCoords(int trackId) async {
|
||||
if (_trackCoords.containsKey(trackId)) return;
|
||||
final pts = await _db.getLatLons(trackId);
|
||||
_trackCoords[trackId] = pts.map((p) => LatLng(p.lat, p.lon)).toList();
|
||||
overlayTrackIds.refresh(); // térkép frissítés
|
||||
}
|
||||
|
||||
void _startWatchdog() {
|
||||
_watchdogTimer = Timer.periodic(
|
||||
const Duration(seconds: 5),
|
||||
(_) => _checkGpsTimeout(),
|
||||
);
|
||||
}
|
||||
|
||||
void _checkGpsTimeout() {
|
||||
if (!isRecording.value || isPaused.value) return;
|
||||
final last = _lastPositionTime;
|
||||
if (last == null) return;
|
||||
|
||||
final elapsedSec = DateTime.now().difference(last).inSeconds;
|
||||
if (elapsedSec < _gpsTimeoutSec) return;
|
||||
|
||||
if (!gpsSignalLost.value) {
|
||||
gpsSignalLost.value = true;
|
||||
|
||||
AppLogger.w(
|
||||
'watchdog',
|
||||
'GPS jel elveszett: ${elapsedSec}s óta nincs pozíció '
|
||||
'(pts: ${livePoints.length}, '
|
||||
'táv: ${_formatDistance(_accumulatedDistance)})');
|
||||
|
||||
AppLogger.w(
|
||||
'gps_signal_lost',
|
||||
'elapsed_sec: ${elapsedSec}'
|
||||
'point_count: ${livePoints.length}'
|
||||
'distance_m: ${_accumulatedDistance.round()}');
|
||||
|
||||
// Foreground notification frissítése
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationTitle: '⚠ GPS jel elveszett',
|
||||
notificationText: 'Track szünetel — folytatódik ha visszajön a jel',
|
||||
);
|
||||
|
||||
// Snackbar ha előtérben van az app
|
||||
if (!isInBackground.value) {
|
||||
Get.snackbar(
|
||||
'GPS jel elveszett',
|
||||
'Valószínűleg alagút vagy lefedettség hiány.\n'
|
||||
'A rögzítés automatikusan folytatódik.',
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.gps_off, color: Colors.white),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Folyamatosan logol amíg nincs jel (percenként egyszer)
|
||||
if (elapsedSec % 60 == 0) {
|
||||
AppLogger.w('watchdog', 'GPS még mindig hiányzik: ${elapsedSec}s');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../controllers/tracking_controller.dart';
|
||||
import '../../../../models/track.dart';
|
||||
|
||||
/// Mentett track-ek listája megtekintési és export lehetőséggel.
|
||||
class TrackListView extends GetView<TrackingController> {
|
||||
const TrackListView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Mentett track-ek'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: controller.loadSavedTracks,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
final tracks = controller.savedTracks;
|
||||
if (tracks.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.route, size: 64, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 12),
|
||||
Text('Még nincs mentett nyomvonal',
|
||||
style: TextStyle(color: Colors.grey.shade500)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: tracks.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, i) => _TrackTile(track: tracks[i]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackTile extends GetView<TrackingController> {
|
||||
final Track track;
|
||||
const _TrackTile({required this.track});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statusColor = switch (track.status) {
|
||||
TrackStatus.recording => Colors.red,
|
||||
TrackStatus.paused => Colors.orange,
|
||||
TrackStatus.finished => Colors.green,
|
||||
};
|
||||
|
||||
return Dismissible(
|
||||
key: ValueKey(track.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (_) => _confirm(context),
|
||||
onDismissed: (_) => controller.deleteTrack(track.id!),
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: statusColor.withOpacity(0.15),
|
||||
child: Icon(Icons.route, color: statusColor),
|
||||
),
|
||||
title: Text(track.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: Text(
|
||||
'${track.distanceFormatted} · ${track.durationFormatted}'
|
||||
' · ${track.pointCount} pont\n${track.source}',
|
||||
),
|
||||
isThreeLine: true,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.map_outlined),
|
||||
tooltip: 'Megtekintés',
|
||||
onPressed: () => Get.to(() => _TrackPreviewView(track: track)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'GPX export',
|
||||
onPressed: () => controller.exportTrack(track),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _confirm(BuildContext context) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Track törlése'),
|
||||
content: Text('"${track.name}" törlése visszavonohatalan.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade50),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Törlés',
|
||||
style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Track előnézeti térkép ──────────────────────────────────────────────────
|
||||
|
||||
class _TrackPreviewView extends GetView<TrackingController> {
|
||||
final Track track;
|
||||
const _TrackPreviewView({required this.track});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(track.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () => controller.exportTrack(track),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<List<LatLng>>(
|
||||
future: controller.loadTrackPoints(track.id!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Hiba: ${snapshot.error}'));
|
||||
}
|
||||
final points = snapshot.data ?? [];
|
||||
if (points.isEmpty) {
|
||||
return const Center(child: Text('Nincs megjeleníthető pont.'));
|
||||
}
|
||||
|
||||
// Bounding box számítás automatikus zoom-hoz
|
||||
final lats = points.map((p) => p.latitude).toList();
|
||||
final lons = points.map((p) => p.longitude).toList();
|
||||
final center = LatLng(
|
||||
(lats.reduce((a, b) => a + b)) / lats.length,
|
||||
(lons.reduce((a, b) => a + b)) / lons.length,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: 14,
|
||||
initialCameraFit: CameraFit.coordinates(
|
||||
coordinates: points,
|
||||
padding: const EdgeInsets.all(40),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'hu.app_dev.terepi_seged',
|
||||
),
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: points,
|
||||
color: Colors.blue.shade700,
|
||||
strokeWidth: 4.0,
|
||||
borderColor: Colors.blue.shade200,
|
||||
borderStrokeWidth: 1.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: points.first,
|
||||
child: const Icon(Icons.flag,
|
||||
color: Colors.green, size: 28),
|
||||
),
|
||||
Marker(
|
||||
point: points.last,
|
||||
child: const Icon(Icons.flag,
|
||||
color: Colors.red, size: 28),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Statisztika sáv alul
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
24, 12, 24, 12 + MediaQuery.of(context).padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_InfoChip(
|
||||
icon: Icons.straighten, text: track.distanceFormatted),
|
||||
_InfoChip(
|
||||
icon: Icons.timer_outlined,
|
||||
text: track.durationFormatted),
|
||||
_InfoChip(
|
||||
icon: Icons.location_on_outlined,
|
||||
text: '${track.pointCount} pont'),
|
||||
_InfoChip(icon: Icons.sensors, text: track.source),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
const _InfoChip({required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 15, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../controllers/tracking_controller.dart';
|
||||
import '../../../../models/track.dart';
|
||||
import 'track_list_view.dart';
|
||||
|
||||
class TrackingView extends GetView<TrackingController> {
|
||||
const TrackingView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
appBar: AppBar(
|
||||
title: const Text('Nyomvonal'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.list_alt),
|
||||
tooltip: 'Mentett track-ek',
|
||||
onPressed: () => Get.to(() => const TrackListView()),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
_LiveMap(),
|
||||
_StatsPanel(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _ControlBar(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Térkép ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _LiveMap extends GetView<TrackingController> {
|
||||
const _LiveMap();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapController = MapController();
|
||||
|
||||
return Obx(() {
|
||||
final points = controller.livePoints;
|
||||
final center = points.isNotEmpty
|
||||
? points.last
|
||||
: const LatLng(47.5, 19.0); // Magyarország közepe
|
||||
|
||||
// Középre követ rögzítés közben
|
||||
if (controller.isRecording.value && points.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
mapController.move(points.last, mapController.camera.zoom);
|
||||
});
|
||||
}
|
||||
|
||||
return FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: 15.0,
|
||||
interactionOptions: const InteractionOptions(
|
||||
flags: InteractiveFlag.all,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'hu.app_dev.terepi_seged',
|
||||
),
|
||||
if (points.length >= 2)
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: points,
|
||||
color: Colors.blue.shade700,
|
||||
strokeWidth: 4.0,
|
||||
borderColor: Colors.blue.shade200,
|
||||
borderStrokeWidth: 1.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (points.isNotEmpty)
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
// Kezdőpont
|
||||
Marker(
|
||||
point: points.first,
|
||||
child: const Icon(Icons.flag, color: Colors.green, size: 28),
|
||||
),
|
||||
// Aktuális pozíció
|
||||
Marker(
|
||||
point: points.last,
|
||||
child: _PulsingDot(
|
||||
color:
|
||||
controller.isPaused.value ? Colors.orange : Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Pulzáló pont az aktuális pozícióhoz.
|
||||
class _PulsingDot extends StatefulWidget {
|
||||
final Color color;
|
||||
const _PulsingDot({required this.color});
|
||||
|
||||
@override
|
||||
State<_PulsingDot> createState() => _PulsingDotState();
|
||||
}
|
||||
|
||||
class _PulsingDotState extends State<_PulsingDot>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _anim;
|
||||
late Animation<double> _scale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_anim = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 1000))
|
||||
..repeat(reverse: true);
|
||||
_scale = Tween(begin: 0.8, end: 1.3)
|
||||
.animate(CurvedAnimation(parent: _anim, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_anim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaleTransition(
|
||||
scale: _scale,
|
||||
child: Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: widget.color.withOpacity(0.5),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 2)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Statisztika panel ────────────────────────────────────────────────────────
|
||||
|
||||
class _StatsPanel extends GetView<TrackingController> {
|
||||
const _StatsPanel();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (!controller.isRecording.value && controller.livePoints.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.92),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_Stat(
|
||||
icon: Icons.timer_outlined,
|
||||
label: 'Idő',
|
||||
value: controller.elapsedFormatted.value,
|
||||
),
|
||||
_Divider(),
|
||||
_Stat(
|
||||
icon: Icons.straighten,
|
||||
label: 'Távolság',
|
||||
value: _formatDist(controller.sessionDistance.value),
|
||||
),
|
||||
_Divider(),
|
||||
_Stat(
|
||||
icon: Icons.speed,
|
||||
label: 'Sebesség',
|
||||
value:
|
||||
'${controller.currentSpeedKmh.value.toStringAsFixed(1)} km/h',
|
||||
),
|
||||
_Divider(),
|
||||
_Stat(
|
||||
icon: Icons.location_on_outlined,
|
||||
label: 'Pontok',
|
||||
value: controller.livePoints.length.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDist(double m) {
|
||||
if (m < 1000) return '${m.toStringAsFixed(0)} m';
|
||||
return '${(m / 1000).toStringAsFixed(2)} km';
|
||||
}
|
||||
}
|
||||
|
||||
class _Stat extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
const _Stat({required this.icon, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 2),
|
||||
Text(value,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
Container(height: 36, width: 1, color: Colors.grey.shade300);
|
||||
}
|
||||
|
||||
// ─── Vezérlő sáv ────────────────────────────────────────────────────────────
|
||||
|
||||
class _ControlBar extends GetView<TrackingController> {
|
||||
const _ControlBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final recording = controller.isRecording.value;
|
||||
final paused = controller.isPaused.value;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2))
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
24, 12, 24, 12 + MediaQuery.of(context).padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (!recording) ...[
|
||||
// ── Nincs aktív track ──
|
||||
_RoundButton(
|
||||
icon: Icons.fiber_manual_record,
|
||||
label: 'Indítás',
|
||||
color: Colors.red,
|
||||
onTap: () => controller.startRecording(),
|
||||
),
|
||||
] else ...[
|
||||
// ── Aktív track ──
|
||||
_RoundButton(
|
||||
icon: paused ? Icons.play_arrow : Icons.pause,
|
||||
label: paused ? 'Folytatás' : 'Szünet',
|
||||
color: Colors.orange,
|
||||
onTap: paused
|
||||
? controller.resumeRecording
|
||||
: controller.pauseRecording,
|
||||
),
|
||||
// Nagy Stop gomb középen
|
||||
GestureDetector(
|
||||
onTap: () => _confirmStop(context),
|
||||
child: Container(
|
||||
width: 68,
|
||||
height: 68,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.red.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2)
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.stop, color: Colors.white, size: 32),
|
||||
),
|
||||
),
|
||||
_RoundButton(
|
||||
icon: Icons.share,
|
||||
label: 'Export',
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
final t = controller.currentTrack.value;
|
||||
if (t != null) controller.exportTrack(t);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmStop(BuildContext context) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Track leállítása'),
|
||||
content: const Text(
|
||||
'Leállítja a rögzítést? A track automatikusan mentésre kerül.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Mégse')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Leállítás')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) controller.stopRecording();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
const _RoundButton(
|
||||
{required this.icon,
|
||||
required this.label,
|
||||
required this.color,
|
||||
required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color, width: 1.5),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:terepi_seged/services/version_gate_service.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:app_settings/app_settings.dart';
|
||||
|
||||
/// Blokkoló "frissítés szükséges" képernyő — ez az EGYETLEN dolog, amit
|
||||
/// az app mutat, ha a build a minimum alatt van. A rendes appot (GetX
|
||||
/// service-ek, routing, shell) ilyenkor a main.dart el sem indítja.
|
||||
class UpdateRequiredView extends StatelessWidget {
|
||||
final VersionGateResult result;
|
||||
const UpdateRequiredView({super.key, required this.result});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.system_update_alt,
|
||||
size: 72, color: Colors.orange),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Frissítés szükséges',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
result.message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
if (result.updateUrl.isNotEmpty)
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('Frissítés'),
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse(result.updateUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.settings_outlined, size: 18),
|
||||
label: const Text('Alkalmazás beállításainak megnyitása'),
|
||||
onPressed: () => AppSettings.openAppSettings(
|
||||
type: AppSettingsType.settings),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'(ha belső tesztelőként a Play Áruház link még nem elérhető)',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:terepi_seged/pages/bleutooth/bindings/bluetooth_bindings.dart';
|
||||
import 'package:terepi_seged/pages/bleutooth/presentation/views/bluetooth_test_view.dart';
|
||||
import 'package:terepi_seged/pages/contacts/presentation/views/contacts_view.dart';
|
||||
import 'package:terepi_seged/pages/field_trip/bindings/field_trip_bindings.dart';
|
||||
import 'package:terepi_seged/pages/field_trip/presentations/views/fiels_trip_view.dart';
|
||||
import 'package:terepi_seged/pages/home/bindings/home_bindings.dart';
|
||||
import 'package:terepi_seged/pages/home/presentation/views/home_view.dart';
|
||||
import 'package:terepi_seged/pages/map/bindings/map_bindings.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/views/map_add_point_dialog.dart';
|
||||
import 'package:terepi_seged/pages/map/presentation/views/map_view.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/bindings/map_survey_bindings.dart';
|
||||
import 'package:terepi_seged/pages/map_survey/presentations/views/map_survey_view.dart';
|
||||
import 'package:terepi_seged/pages/measured_data/bindings/measured_data_bindings.dart';
|
||||
import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart';
|
||||
import 'package:terepi_seged/pages/navigation/bindings/navigation_bindings.dart';
|
||||
@@ -14,14 +19,21 @@ import 'package:terepi_seged/pages/property_list/bindings/property_list_bindings
|
||||
import 'package:terepi_seged/pages/property_list/presentations/views/property_list_view.dart';
|
||||
import 'package:terepi_seged/pages/rtcm_test/bindings/rtcm_test_bindings.dart';
|
||||
import 'package:terepi_seged/pages/rtcm_test/presentation/views/rtcm_test_view.dart';
|
||||
import 'package:terepi_seged/pages/settings/presentation/views/settings_view.dart';
|
||||
import 'package:terepi_seged/pages/socket_test/bindings/socket_test_bindings.dart';
|
||||
import 'package:terepi_seged/pages/socket_test/presentation/views/socket_test_view.dart';
|
||||
import 'package:terepi_seged/pages/stakeout_import/presentation/views/stakeout_import_view.dart';
|
||||
import 'package:terepi_seged/pages/start/bindings/start_page_bindings.dart';
|
||||
import 'package:terepi_seged/pages/start/presentation/views/start_page.dart';
|
||||
import 'package:terepi_seged/pages/tracking/bindings/tracking_bindings.dart';
|
||||
import 'package:terepi_seged/pages/tracking/presentation/views/tracking_view.dart';
|
||||
|
||||
import '../pages/map_test/bindings/map_test_bindings.dart';
|
||||
import '../pages/map_test/presentation/views/map_test_view.dart';
|
||||
|
||||
import '../pages/shell/bindings/shell_binding.dart';
|
||||
import '../pages/shell/presentations/views/shell_view.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
// ignore: avoid_classes_with_only_static_members
|
||||
@@ -40,6 +52,11 @@ class AppPages {
|
||||
name: Routes.STARTPAGE,
|
||||
page: () => const StartPage(),
|
||||
binding: StartPageBinding()),
|
||||
GetPage(
|
||||
name: Routes.SHELL,
|
||||
page: () => const ShellView(),
|
||||
binding: ShellBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.MAP, page: () => const MapView(), binding: MapBinding()),
|
||||
GetPage(
|
||||
@@ -73,6 +90,22 @@ class AppPages {
|
||||
GetPage(
|
||||
name: Routes.MEASUREDDATA,
|
||||
binding: MeasuredDataBinding(),
|
||||
page: () => const MeasuredDataView())
|
||||
page: () => const MeasuredDataView()),
|
||||
GetPage(
|
||||
name: Routes.MAPSURVEY,
|
||||
binding: MapSurveyBinding(),
|
||||
page: () => const MapSurveyView()),
|
||||
GetPage(
|
||||
name: Routes.FIELDTRIP,
|
||||
binding: FieldTripBinding(),
|
||||
page: () => const FieldTripView()),
|
||||
GetPage(
|
||||
name: Routes.TRACKING,
|
||||
binding: TrackingBinding(),
|
||||
page: () => const TrackingView()),
|
||||
GetPage(name: Routes.SETTINGS, page: () => const SettingsView()),
|
||||
GetPage(
|
||||
name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()),
|
||||
GetPage(name: Routes.CONTACTS, page: () => const ContactsView())
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@ abstract class Routes {
|
||||
static const MAPTEST = '/map_test';
|
||||
static const PROPERTYLIST = '/property_list';
|
||||
static const MEASUREDDATA = '/measured_data';
|
||||
static const MAPSURVEY = '/map_survey';
|
||||
static const FIELDTRIP = '/field_trip';
|
||||
static const TRACKING = '/tracking';
|
||||
|
||||
static const MAPADDPOINTDIALOG = "/map_add_point_dialog";
|
||||
|
||||
static const LOGIN = '/login';
|
||||
static const SHELL = '/shell';
|
||||
static const CONTACTS = '/contacts';
|
||||
|
||||
static const SETTINGS = '/settings';
|
||||
static const STAKEOUT_IMPORT = '/stakeout_import';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
// Fejlesztési log service — fájlba írás terepi teszteléshez.
|
||||
// Az app külső tárhelyére ír, onnan könnyen elérhető.
|
||||
//
|
||||
// Használat:
|
||||
// AppLogger.i('startRecording', 'Track indítva: $name');
|
||||
// AppLogger.w('_onPosition', 'Ugrás kiszűrve: ${dist}m');
|
||||
// AppLogger.e('_onPosition', 'GPS hiba', error: e);
|
||||
//
|
||||
// Log fájl helye: /sdcard/Android/data/hu.app_dev.terepi_seged/files/logs/
|
||||
// Elérhető: Android Studio Device Explorer, adb pull, vagy fájlkezelő app
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
enum _Level { info, warning, error }
|
||||
|
||||
class AppLogger extends GetxService {
|
||||
static AppLogger get to => Get.find();
|
||||
|
||||
// ── Konfiguráció ──────────────────────────────────────────────────
|
||||
|
||||
/// Csak fejlesztési módban logol (release build-ben kikapcsol)
|
||||
static const bool _enableInRelease = true;
|
||||
|
||||
/// Max log fájl méret MB-ban — felette rotál
|
||||
static const int _maxSizeMb = 5;
|
||||
|
||||
/// Max megőrzött log fájlok száma
|
||||
static const int _maxFiles = 5;
|
||||
|
||||
// ── Belső állapot ─────────────────────────────────────────────────
|
||||
|
||||
File? _logFile;
|
||||
IOSink? _sink;
|
||||
String? _logDir;
|
||||
|
||||
static final _timeFmt = DateFormat('HH:mm:ss.SSS');
|
||||
static final _dateFmt = DateFormat('yyyy-MM-dd');
|
||||
static final _fileFmt = DateFormat('yyyy-MM-dd_HH-mm-ss');
|
||||
|
||||
final isEnabled = false.obs;
|
||||
final currentLogPath = ''.obs;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> onReady() async {
|
||||
super.onReady();
|
||||
|
||||
if (!kDebugMode && !_enableInRelease) return;
|
||||
|
||||
try {
|
||||
await _initLogFile();
|
||||
isEnabled.value = true;
|
||||
i('AppLogger', 'Log indítva — ${currentLogPath.value}');
|
||||
} catch (e) {
|
||||
debugPrint('AppLogger init hiba: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onClose() async {
|
||||
await _sink?.flush();
|
||||
await _sink?.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ── Publikus API ──────────────────────────────────────────────────
|
||||
|
||||
/// Info szintű log
|
||||
static void i(String tag, String message) =>
|
||||
_write(_Level.info, tag, message);
|
||||
|
||||
/// Figyelmeztetés
|
||||
static void w(String tag, String message, {Object? error}) =>
|
||||
_write(_Level.warning, tag, message, error: error);
|
||||
|
||||
/// Hiba
|
||||
static void e(String tag, String message,
|
||||
{Object? error, StackTrace? stack}) =>
|
||||
_write(_Level.error, tag, message, error: error, stack: stack);
|
||||
|
||||
/// Szeparátor — jól látható elválasztó a logban
|
||||
static void separator(String label) {
|
||||
_write(_Level.info, '─────', '─── $label ───────────────────────');
|
||||
}
|
||||
|
||||
// ── Log fájl megosztása ───────────────────────────────────────────
|
||||
|
||||
Future<void> shareLogs() async {
|
||||
await _sink?.flush();
|
||||
|
||||
final dir = _logDir;
|
||||
if (dir == null) return;
|
||||
|
||||
final files = Directory(dir)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.endsWith('.log'))
|
||||
.toList()
|
||||
..sort((a, b) => b.path.compareTo(a.path));
|
||||
|
||||
if (files.isEmpty) {
|
||||
Get.snackbar('Log', 'Nincs log fájl',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
return;
|
||||
}
|
||||
|
||||
// Legutóbbi fájl megosztása
|
||||
await SharePlus.instance.share(ShareParams(
|
||||
files: [XFile(files.first.path, mimeType: 'text/plain')],
|
||||
subject: 'Terepi Segéd log — ${_dateFmt.format(DateTime.now())}',
|
||||
));
|
||||
}
|
||||
|
||||
/// Összes log fájl törlése
|
||||
Future<void> clearLogs() async {
|
||||
await _sink?.flush();
|
||||
await _sink?.close();
|
||||
_sink = null;
|
||||
|
||||
final dir = _logDir;
|
||||
if (dir == null) return;
|
||||
|
||||
for (final f in Directory(dir).listSync().whereType<File>()) {
|
||||
await f.delete().catchError((_) {});
|
||||
}
|
||||
|
||||
await _initLogFile();
|
||||
i('AppLogger', 'Logok törölve, új fájl nyitva');
|
||||
}
|
||||
|
||||
// ── Belső ─────────────────────────────────────────────────────────
|
||||
|
||||
static void _write(
|
||||
_Level level,
|
||||
String tag,
|
||||
String message, {
|
||||
Object? error,
|
||||
StackTrace? stack,
|
||||
}) {
|
||||
if (!kDebugMode && !_enableInRelease) return;
|
||||
|
||||
// Konzolon is megjelenik
|
||||
final prefix = switch (level) {
|
||||
_Level.info => '📋',
|
||||
_Level.warning => '⚠️',
|
||||
_Level.error => '🔴',
|
||||
};
|
||||
debugPrint('$prefix [$tag] $message'
|
||||
'${error != null ? ' | $error' : ''}');
|
||||
|
||||
// Fájlba írás
|
||||
try {
|
||||
AppLogger.to._writeLine(level, tag, message, error: error, stack: stack);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _writeLine(
|
||||
_Level level,
|
||||
String tag,
|
||||
String message, {
|
||||
Object? error,
|
||||
StackTrace? stack,
|
||||
}) {
|
||||
final sink = _sink;
|
||||
if (sink == null) return;
|
||||
|
||||
final time = _timeFmt.format(DateTime.now());
|
||||
final lvl = switch (level) {
|
||||
_Level.info => 'I',
|
||||
_Level.warning => 'W',
|
||||
_Level.error => 'E',
|
||||
};
|
||||
|
||||
sink.writeln('$time $lvl [$tag] $message');
|
||||
if (error != null) sink.writeln(' ↳ $error');
|
||||
if (stack != null) {
|
||||
// Csak az első 5 sor a stack trace-ből
|
||||
final lines = stack.toString().split('\n').take(5);
|
||||
for (final l in lines) sink.writeln(' $l');
|
||||
}
|
||||
|
||||
// Méret ellenőrzés — ha szükséges, rotál
|
||||
_checkRotation();
|
||||
}
|
||||
|
||||
Future<void> _initLogFile() async {
|
||||
final ext = await getExternalStorageDirectory();
|
||||
final dir = Directory(p.join(ext!.path, 'logs'));
|
||||
await dir.create(recursive: true);
|
||||
_logDir = dir.path;
|
||||
|
||||
final name = 'terepi_${_fileFmt.format(DateTime.now())}.log';
|
||||
_logFile = File(p.join(dir.path, name));
|
||||
_sink = _logFile!.openWrite(mode: FileMode.append);
|
||||
currentLogPath.value = _logFile!.path;
|
||||
|
||||
// Fejléc
|
||||
_sink!.writeln('═' * 60);
|
||||
_sink!.writeln('Terepi Segéd — Log');
|
||||
_sink!.writeln('Dátum: ${DateTime.now().toIso8601String()}');
|
||||
_sink!.writeln('═' * 60);
|
||||
|
||||
// Régi fájlok törlése
|
||||
await _pruneOldFiles(dir);
|
||||
}
|
||||
|
||||
void _checkRotation() {
|
||||
final file = _logFile;
|
||||
if (file == null) return;
|
||||
if (!file.existsSync()) return;
|
||||
|
||||
final sizeMb = file.lengthSync() / (1024 * 1024);
|
||||
if (sizeMb > _maxSizeMb) {
|
||||
_sink?.flush();
|
||||
_sink?.close();
|
||||
_initLogFile(); // új fájl
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pruneOldFiles(Directory dir) async {
|
||||
final files = dir
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.endsWith('.log'))
|
||||
.toList()
|
||||
..sort((a, b) => a.path.compareTo(b.path));
|
||||
|
||||
while (files.length > _maxFiles) {
|
||||
await files.removeAt(0).delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:terepi_seged/services/app_logger.dart';
|
||||
|
||||
/// Ember által olvasható hitelesítési hiba.
|
||||
class AuthServiceException implements Exception {
|
||||
final String message;
|
||||
AuthServiceException(this.message);
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Hitelesítési service — google_sign_in 7.x + Supabase.
|
||||
///
|
||||
/// Folyamat: natív Google bejelentkezés → idToken (+ accessToken az
|
||||
/// authorizationClient-ből) → Supabase signInWithIdToken → Supabase
|
||||
/// session (amit a supabase_flutter magától perzisztál és frissít, ezért
|
||||
/// app-újraindításnál NEM kell újra Google-bejelentkezés).
|
||||
///
|
||||
/// FONTOS (google_sign_in 7.x változások a 6.x-hez képest):
|
||||
/// * singleton: GoogleSignIn.instance (nincs konstruktor-példányosítás)
|
||||
/// * kötelező egyszeri initialize() minden más hívás előtt
|
||||
/// * signIn() helyett authenticate(); a megszakítást kivétel jelzi
|
||||
/// * az authentication már csak idToken-t ad; accessToken a
|
||||
/// authorizationClient-től kérhető
|
||||
///
|
||||
/// A GetX-minta szerint az állapot Rx-ekben él: a UI (drawer-fejléc,
|
||||
/// beállítások) Obx-szel figyeli a currentUser-t.
|
||||
class AuthService extends GetxService {
|
||||
static AuthService get to => Get.find();
|
||||
|
||||
SupabaseClient get _supabase => Supabase.instance.client;
|
||||
|
||||
// ── Reaktív állapot ──────────────────────────────────────────────
|
||||
final currentUser = Rxn<User>();
|
||||
final isBusy = false.obs;
|
||||
|
||||
bool get isSignedIn => currentUser.value != null;
|
||||
|
||||
String get displayName {
|
||||
final u = currentUser.value;
|
||||
if (u == null) return 'Nincs bejelentkezve';
|
||||
return (u.userMetadata?['full_name'] as String?) ??
|
||||
(u.userMetadata?['name'] as String?) ??
|
||||
u.email ??
|
||||
'Felhasználó';
|
||||
}
|
||||
|
||||
String get email => currentUser.value?.email ?? '';
|
||||
|
||||
String? get avatarUrl =>
|
||||
currentUser.value?.userMetadata?['avatar_url'] as String?;
|
||||
|
||||
bool _googleInitialized = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// A Supabase a sessiont maga perzisztálja — induláskor csak átvesszük.
|
||||
currentUser.value = _supabase.auth.currentUser;
|
||||
|
||||
// Minden auth-változás (bejelentkezés, kijelentkezés, token-frissítés)
|
||||
// ide fut be; a UI a currentUser-en keresztül reagál.
|
||||
_supabase.auth.onAuthStateChange.listen((data) {
|
||||
currentUser.value = data.session?.user;
|
||||
}, onError: (Object error, StackTrace stack) {
|
||||
if (kDebugMode) debugPrint('[Auth] onAuthStateChange hiba: $error');
|
||||
AppLogger.e('Auth', '[Auth] onAuthStateChange hiba: $error');
|
||||
});
|
||||
}
|
||||
|
||||
/// Egyszeri google_sign_in inicializálás (a 7.x megköveteli).
|
||||
/// A serverClientId a Supabase-ben engedélyezett WEB client ID —
|
||||
/// az idToken "közönsége" (aud) így egyezik a Supabase elvárásával.
|
||||
Future<void> _ensureGoogleInitialized() async {
|
||||
if (_googleInitialized) return;
|
||||
final webClientId = dotenv.env['GOOGLE_WEB_CLIENT_ID'];
|
||||
if (webClientId == null || webClientId.isEmpty) {
|
||||
throw AuthServiceException(
|
||||
'Hiányzó GOOGLE_WEB_CLIENT_ID a .env fájlból.');
|
||||
}
|
||||
await GoogleSignIn.instance.initialize(serverClientId: webClientId);
|
||||
_googleInitialized = true;
|
||||
}
|
||||
|
||||
/// Google bejelentkezés → Supabase session.
|
||||
/// Visszatérés: true = bejelentkezve; false = a felhasználó megszakította.
|
||||
/// Egyéb hibánál [AuthServiceException]-t dob, magyar üzenettel.
|
||||
Future<bool> signInWithGoogle() async {
|
||||
if (isBusy.value) return false;
|
||||
isBusy.value = true;
|
||||
try {
|
||||
await _ensureGoogleInitialized();
|
||||
|
||||
// Interaktív bejelentkezés — megszakítást kivétel jelzi (7.x).
|
||||
final GoogleSignInAccount account;
|
||||
try {
|
||||
account = await GoogleSignIn.instance.authenticate();
|
||||
} on GoogleSignInException catch (e) {
|
||||
if (e.code == GoogleSignInExceptionCode.canceled) return false;
|
||||
throw AuthServiceException('Google-bejelentkezési hiba: '
|
||||
'${e.description ?? e.code.name}');
|
||||
}
|
||||
|
||||
final idToken = account.authentication.idToken;
|
||||
if (idToken == null) {
|
||||
throw AuthServiceException(
|
||||
'A Google nem adott vissza idToken-t — ellenőrizd a '
|
||||
'GOOGLE_WEB_CLIENT_ID beállítást.');
|
||||
}
|
||||
|
||||
// accessToken a 7.x-ben külön engedélyezési lépés (a Supabase
|
||||
// Google idToken-flow-ja kéri).
|
||||
const scopes = ['email', 'profile'];
|
||||
final authorization =
|
||||
await account.authorizationClient.authorizationForScopes(scopes) ??
|
||||
await account.authorizationClient.authorizeScopes(scopes);
|
||||
|
||||
await _supabase.auth.signInWithIdToken(
|
||||
provider: OAuthProvider.google,
|
||||
idToken: idToken,
|
||||
accessToken: authorization.accessToken,
|
||||
);
|
||||
|
||||
// currentUser-t az onAuthStateChange listener állítja be.
|
||||
if (kDebugMode) {
|
||||
debugPrint('[Auth] Bejelentkezve: ${account.email}');
|
||||
}
|
||||
return true;
|
||||
} on AuthException catch (e) {
|
||||
throw AuthServiceException('Supabase hitelesítési hiba: ${e.message}');
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Kijelentkezés mindkét oldalról (Supabase session + Google fiók).
|
||||
Future<void> signOut() async {
|
||||
isBusy.value = true;
|
||||
try {
|
||||
await _supabase.auth.signOut();
|
||||
if (_googleInitialized) {
|
||||
try {
|
||||
await GoogleSignIn.instance.signOut();
|
||||
} catch (_) {
|
||||
// A Google-oldali kijelentkezés hibája nem kritikus.
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'dart:async';
|
||||
import 'location_source.dart';
|
||||
|
||||
/// BLE GNSS vevőből érkező helymeghatározási forrás.
|
||||
///
|
||||
/// A meglévő Bluetooth + NMEA parsing logikát köti be a
|
||||
/// [LocationSource] interfészbe, így a TrackingController
|
||||
/// forrásváltás nélkül tud működni.
|
||||
///
|
||||
/// TEENDŐK a BLE verzió elkészültével:
|
||||
/// 1. Injektáld a BluetoothConnection referenciát (vagy egy
|
||||
/// stream-et a NMEA mondatokból).
|
||||
/// 2. Parsold a GNGGA mondatokat (ugyanaz a [Gngga] osztály
|
||||
/// ami a MapSurveyController-ben is megvan).
|
||||
/// 3. Alkalmazd a geoid-korrekciót [GeoidGrid] segítségével.
|
||||
/// 4. Töltsd fel a [SourcePosition]-t a korrekt mezőkkel.
|
||||
class BleGnssSource implements LocationSource {
|
||||
@override
|
||||
String get displayName => 'BLE GNSS';
|
||||
|
||||
// TODO: Stream<String> nmeaStream — a BLE controller adja
|
||||
final Stream<String>? nmeaStream;
|
||||
|
||||
StreamController<SourcePosition>? _controller;
|
||||
StreamSubscription? _sub;
|
||||
|
||||
BleGnssSource({this.nmeaStream});
|
||||
|
||||
@override
|
||||
bool get isAvailable => _sub != null;
|
||||
|
||||
@override
|
||||
Stream<SourcePosition> get positionStream {
|
||||
_controller = StreamController<SourcePosition>.broadcast();
|
||||
|
||||
if (nmeaStream == null) {
|
||||
_controller!.addError(Exception(
|
||||
'BLE GNSS forrás nincs bekötve. '
|
||||
'Adj meg egy nmeaStream-et a konstruktorban.',
|
||||
));
|
||||
return _controller!.stream;
|
||||
}
|
||||
|
||||
_sub = nmeaStream!.listen((line) {
|
||||
if (!line.startsWith('\$GNGGA')) return;
|
||||
|
||||
// TODO: Gngga parser + GeoidGrid korreckció beépítése
|
||||
// Példa váz:
|
||||
//
|
||||
// final sentence = nmeaDecoder.decode(line);
|
||||
// if (sentence is! Gngga || !sentence.valid) return;
|
||||
// final ellipsoidal =
|
||||
// sentence.altitudeAboveMeanSeaLevel + sentence.geoidSeparation;
|
||||
// final eovZ = geoidGrid.toEovHeight(
|
||||
// sentence.latitude, sentence.longitude,
|
||||
// sentence.altitudeAboveMeanSeaLevel, sentence.geoidSeparation);
|
||||
//
|
||||
// _controller?.add(SourcePosition(
|
||||
// latitude: sentence.latitude,
|
||||
// longitude: sentence.longitude,
|
||||
// altitude: eovZ ?? ellipsoidal,
|
||||
// accuracy: null, // GNGST-ből lehetne
|
||||
// timestamp: DateTime.now(),
|
||||
// source: displayName,
|
||||
// ));
|
||||
});
|
||||
|
||||
return _controller!.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _sub?.cancel();
|
||||
await _controller?.close();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user