82 Commits
Author SHA1 Message Date
gitea_admin fc31acb80f Kml parser utf-8 támogatás, induláskor minimum app verzió ellenőrzése 2026-07-09 03:43:06 +02:00
gitea_admin 1e5601a10b Rétegek projektjének helyes kezelése 2026-07-09 00:31:17 +02:00
gitea_admin 9f068b79a4 Gitea beállítás a gitlens-hez 2026-07-09 00:17:46 +02:00
gitea_admin d23f736ead Track online szinkronizáció refraktorálása 2026-07-08 23:29:22 +02:00
gitea_admin 1ab656585b Kontaktok listázási hibájának javítása, csv export 2026-07-08 22:13:30 +02:00
gitea_admin 7cd682fe59 onAuthStateChange kezeletlen hiba módosítása, rögzítése 2026-07-08 15:41:29 +02:00
gitea_admin f523053fb3 AppDatabase tesztek 2026-07-08 15:06:16 +02:00
gitea_admin d7bbbcae77 Kitűzési panel szövegmegjelenítési hibájának javítása 2026-07-08 14:47:32 +02:00
gitea_admin b1efaff34a Kapcsolat menü és a hozzá tartozó funkciók. 2026-07-08 13:32:04 +02:00
gitea_admin 7b35ed2939 AppDatabse sql create hiba javítása 2026-07-08 10:42:58 +02:00
gitea_admin 07908ecaa1 Adatbázis és szinkronizációs hibajavítások 2026-07-07 03:25:51 +02:00
gitea_admin d3857f7fde Libella megjelnítése mérőrúdhoz, transzverzális rajzolása az aktuális kitűzendő ponthoz 2026-07-07 02:48:37 +02:00
gitea_admin ecb4892bc8 Közös projekt szinkronizációja 2026-07-07 02:21:08 +02:00
gitea_admin 2a2ce4bb92 Kitűzés: kényelmi funkciók, pontok listája, export 2026-07-06 14:26:16 +02:00
gitea_admin 6706b2b1ba Kitűzés: pontok importja, szervízek, kitüző panel 2026-07-06 11:47:41 +02:00
gitea_admin c01fdcf012 Google authentikáció bevezetése 2026-07-06 09:53:33 +02:00
gitea_admin 2bd39fbd4e Ntrip beállítások sheet, ntrip státusz sheet 2026-07-05 15:48:33 +02:00
gitea_admin fa25a320eb Ntrip szervíz refraktorálás, beállítások, jelszó secure store-ba 2026-07-05 03:43:07 +02:00
gitea_admin 80f7c8d571 Távolság vagy terület mérése a térképen terepbejárás nézetben 2026-07-05 01:41:15 +02:00
gitea_admin 2dc7768f5f Kibővített színválasztó widget: 24 szín és az utolsó 5 kiemelve 2026-07-05 00:10:55 +02:00
gitea_admin 2cf83149f0 Importált rétegek stílusának szerkesztése 2026-07-04 23:36:01 +02:00
gitea_admin 077b40967c SQLite adatbázis: foreign_keys konfigurálása az adatbázis megnyitásakor 2026-07-04 17:22:10 +02:00
gitea_admin 968b9d8b10 Terepbejárás: rögzített geometriai rétegek láthatóságának ki- és bekapcsolása 2026-07-04 17:13:47 +02:00
gitea_admin f130f583ba Track látszik a terepi bejárás térképen is, gps log jelvesztéskor. 2026-07-02 15:49:23 +02:00
gitea_admin e126f340c6 Firebase analytics és crashlytics használatának beállítása a FirebaseLogger-ben. 2026-06-24 11:24:19 +02:00
gitea_admin ccf62aafce AppLogger service hozzáadása 2026-06-23 15:55:33 +02:00
gitea_admin aa78c7bb6f Online tracking, deviceidentityservice 2026-06-23 15:21:20 +02:00
gitea_admin 65b355edd9 Bejárás: pont színének megadása. Projekt export geopackage formátumban 2026-06-21 16:35:09 +02:00
gitea_admin 729aa9bc7f Bejárás mód: pont geometria szerkesztése, új hely 2026-06-21 14:17:41 +02:00
gitea_admin 32821dabc7 Bejárás mód: minden rögzített geometria megjelenítése egy listában. 2026-06-21 13:52:48 +02:00
gitea_admin 026d7799f9 Bejárás mód: pont leírás mentése 2026-06-21 12:57:17 +02:00
gitea_admin 52bce4b925 Bemérés nézet: pont bemérése, törlése, lista, mentés sqlite-ba, exportálás 2026-06-21 12:33:57 +02:00
gitea_admin 7c29bc7ae5 Kml, kmz és geojson import és térképi megjelnítésük. 2026-06-21 10:07:35 +02:00
gitea_admin 0828630a5b Terepbejárás geometria hang és képi dokumentáció létrehozása. Gradle verzió frissítése 2026-06-20 22:29:15 +02:00
gitea_admin ab5ce48f9c Android natív kód: methodchannel az eszköznév lekérdezésére 2026-06-20 01:43:27 +02:00
gitea_admin 9656bbd332 Terepbejárás: geometriák hosszának és területének számítása és jelzése 2026-06-20 01:02:50 +02:00
gitea_admin a9316b9b8d Aktuális pozíciót jelzésének javítása 2026-06-20 00:15:41 +02:00
gitea_admin 0257beec38 Terepbejárás geometriák szerkesztése: kontrollpontok és tulajdonságok. 2026-06-19 12:53:50 +02:00
gitea_admin 1276ac0610 Terepbejárás oldalon vonal és terület tulajdonságainak szerkesztése, mentés 2026-06-16 14:12:44 +02:00
gitea_admin 537897005c Track rögzítés vizuális visszajelzésének módosítása. 2026-06-16 02:06:13 +02:00
gitea_admin beb07fe007 Térképi rétegkezelő lérehozása, track online mentésének előkészítése 2026-06-12 13:41:36 +02:00
gitea_admin 644201dc8a Aktív projekt kiválasztása, projekt létrehozása 2026-06-12 10:59:38 +02:00
gitea_admin ad46429aa9 Projekt modellhez alapértelmezett oszlop hozzáadása 2026-06-12 09:10:02 +02:00
gitea_admin 71305e49f9 Appbar módosítás, projektnév az alcímben 2026-06-12 09:04:02 +02:00
gitea_admin 43af5f5bc8 Appbar átalakítása 2026-06-12 00:17:24 +02:00
gitea_admin 01e6105240 Tracking funkció fejlesztése 2026-06-11 01:20:55 +02:00
gitea_admin 2364b2311c Adatbázis új struktúra és könyvtár, projektrendszer bevezetése, ProjectService 2026-06-10 15:17:26 +02:00
gitea_admin 8b4c25f475 GNSS minőség adatok kijelzése (pod-ok) 2026-06-09 00:23:28 +02:00
gitea_admin b85cdb2596 wgs84 és eov koordinátákat megjelenítő kártyák, eov magasság 2026-06-07 11:45:15 +02:00
gitea_admin 497821bb41 Látható és használt műholdak számának meghatározása és megjelenítése 2026-06-06 23:16:03 +02:00
gitea_admin 9347e22843 Új nmea mondatok: gsa, gsv, műhold info osztály 2026-05-28 13:24:16 +02:00
gitea_admin 3d4c937b71 Új appbar, NtripSettingsSheet 2026-05-27 15:04:46 +02:00
gitea_admin ce8b539be3 MapSurvey - refraktorálás 2026-05-24 14:50:31 +02:00
gitea_admin 53888c9dcb GGA, GST és RMC nmea mondat értelemző refraktorálás 2026-05-19 00:37:18 +02:00
gitea_admin 20a8078f3c GnssService javítás, refraktálás 2026-05-19 00:10:41 +02:00
gitea_admin 24a6b7d513 GNSS szervíz refraktorálás, hibajavítás, PhoneGpsConnection osztály 2026-05-17 15:04:48 +02:00
gitea_admin ee0f90e247 Térkép refraktorálás 2026-05-17 00:35:21 +02:00
gitea_admin d50a324e44 SharedMapWidget és térkép nézet refraktorálása 2026-05-16 14:47:34 +02:00
gitea_admin f2457817b2 Új térkép widget, melyet több oldal is használ 2026-05-12 00:14:11 +02:00
gitea_admin 7dd642b299 A dotenv csomag használata az érzékeny adatok tárolásához. 2026-05-11 14:07:13 +02:00
gitea_admin af5d9d2c0b GnssService osztály és refraktorálás a app_drawer miatt, shellview bevezetés. 2026-05-11 13:50:40 +02:00
gitea_admin f1a3753f36 GNSS eszközhöz való kapcsolódás és bluetooth kommunikáció refraktorálása külön osztályokba 2026-05-11 09:34:41 +02:00
gitea_admin 7192fa5322 Track, nyomkövetés hozzáadása 2026-05-10 02:31:27 +02:00
gitea_admin 810e118059 Hibajavítás: CoordConverterService 2026-05-09 23:14:18 +02:00
gitea_admin 48413ba344 MapController: ha nincs külső GPS csatlakoztatva, akkor a belsőt használja 2026-05-09 23:01:00 +02:00
gitea_admin 81f1fa5cd7 Proj4 refraktorálás a CoordConverterService osztályba. MapController refraktorálás. 2026-05-09 22:04:14 +02:00
gitea_admin 7ee47e65bd Eov magassági korrekció rácshálóból (gtx) 2026-05-09 21:48:14 +02:00
gitea_admin 688922808b A map_survey-ben a bemért pontok mentése és elküldése. Hibajavítás: a field_trip-ben vonalak és területek megjelenítése szerkesztés után. 2026-05-09 15:36:58 +02:00
gitea_admin ee4fd6d059 Update gradle java version to 17. 2025-12-05 12:11:46 +01:00
gitea_admin aba992d8a2 Share measured points file 2025-12-05 08:37:12 +01:00
gitea_admin 061e64fe98 Save measured point to file 2025-12-04 14:31:07 +01:00
gitea_admin 7e035f1414 GPS data save to supabase on the navigation screen #2 2025-10-11 23:13:05 +02:00
gitea_admin e8a6e4464a Start to implement functions: adding point, polyline and polygon to map 2025-10-11 09:26:59 +02:00
gitea_admin e986cccdd6 Add folders and files for field trip and tracking functions 2025-10-05 14:34:46 +02:00
gitea_admin cb94aa042a Update flutter_map package and dependencies, remove flutter_map_geojson 2025-10-05 10:41:07 +02:00
gitea_admin 1293176f89 Add view to show measured point in a list 2025-09-12 04:09:42 +02:00
gitea_admin 057f4a4809 Add toggle button to show or hide password in MapSurveyView Settings page 2025-09-11 08:56:17 +02:00
gitea_admin a3b2deec21 Add map survey view - to measure a point 2025-09-10 14:46:51 +02:00
torok.istvan 6c7ceac1a6 Navigációs fejlesztések, kitűzési fejlesztések 2025-03-10 13:33:21 +01:00
torok.istvan a46674844c Update environment generated file 2025-02-21 08:26:27 +01:00
torok.istvan ff9636b75b Migrate 2025-02-21 08:20:41 +01:00
torok.istvan 1be6b0681f Migrate to new gradle build version 2025-02-21 00:13:25 +01:00
201 changed files with 32397 additions and 453 deletions
+5
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
SUPABASE_URL=
SUPABASE_ANON_KEY=
GOOGLE_WEB_CLIENT_ID=
+1
View File
@@ -69,6 +69,7 @@ unlinked_spec.ds
**/android/**/GeneratedPluginRegistrant.java **/android/**/GeneratedPluginRegistrant.java
**/android/key.properties **/android/key.properties
*.jks *.jks
**/android/app/.cxx/
# iOS/XCode related # iOS/XCode related
**/ios/**/*.mode1v3 **/ios/**/*.mode1v3
+15 -1
View File
@@ -1,4 +1,18 @@
{ {
"java.configuration.updateBuildConfiguration": "interactive", "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"
}
]
} }
+4
View File
@@ -11,3 +11,7 @@ GeneratedPluginRegistrant.java
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
.env
.env.*
!.env.example
+29 -45
View File
@@ -1,30 +1,10 @@
def localProperties = new Properties() plugins {
def localPropertiesFile = rootProject.file('local.properties') id "com.android.application"
if (localPropertiesFile.exists()) { id "kotlin-android"
localPropertiesFile.withReader('UTF-8') { reader -> id "dev.flutter.flutter-gradle-plugin"
localProperties.load(reader) 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 keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
@@ -33,20 +13,23 @@ if(keystorePropertiesFile.exists()){
} }
android { android {
compileSdkVersion 33 namespace = "hu.app_dev.terepi_seged"
ndkVersion "25.1.8937393" compileSdk 36
// ndkVersion "25.1.8937393"
// ndkVersion flutter.ndkVersion
//ndkVersion "27.0.12077973"
ndkVersion "28.2.13676358"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 // sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 // targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' // jvmTarget = JavaVersion.VERSION_1_8
} jvmTarget = JavaVersion.VERSION_17
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
} }
defaultConfig { defaultConfig {
@@ -54,11 +37,13 @@ android {
applicationId "hu.app_dev.terepi_seged" applicationId "hu.app_dev.terepi_seged"
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 23 minSdkVersion 26
targetSdkVersion 33 targetSdkVersion 35
versionCode flutterVersionCode.toInteger() versionCode flutter.versionCode
versionName flutterVersionName versionName flutter.versionName
multiDexEnabled true
// archivesBaseName = "terepi_seged-${versionName}-${new Date().format('yyyyMMdd-HHmm')}"
// multiDexEnabled true
} }
signingConfigs { signingConfigs {
@@ -83,8 +68,7 @@ flutter {
source '../..' source '../..'
} }
dependencies { // dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // implementation platform('com.google.firebase:firebase-bom:31.3.0')
implementation platform('com.google.firebase:firebase-bom:31.3.0') // implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx' // }
}
+24
View File
@@ -13,6 +13,30 @@
} }
}, },
"oauth_client": [ "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_id": "34126164738-s3jjdhb3htkv6l18bqbgi53ebehhh4mp.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
+30 -5
View File
@@ -5,6 +5,17 @@
android:name="${applicationName}" android:name="${applicationName}"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"> 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 <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -33,21 +44,35 @@
android:value="2" /> android:value="2" />
</application> </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.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_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" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_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.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <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" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" 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" tools:remove="android:maxSdkVersion"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" 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_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
</manifest> </manifest>
@@ -1,6 +1,59 @@
package hu.app_dev.terepi_seged package hu.app_dev.terepi_seged
import android.os.Build
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() { 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
View File
@@ -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 { allprojects {
repositories { repositories {
google() google()
@@ -25,6 +11,18 @@ allprojects {
} }
rootProject.buildDir = '../build' rootProject.buildDir = '../build'
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
if (namespace == null) {
namespace project.group
}
}
}
}
}
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
} }
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=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
View File
@@ -1,6 +1,7 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
+24 -8
View File
@@ -1,11 +1,27 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties() def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk") def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
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"
+3
View File
@@ -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:
+9
View File
@@ -24,6 +24,15 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <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> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
+67
View File
@@ -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
}
}
+414
View File
@@ -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';
}
}
}
+105
View File
@@ -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);
}
+17
View File
@@ -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)}';
}
return '${(squareMeters / 10000.0).toStringAsFixed(2)} ha';
}
}
+550
View File
@@ -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),
];
}
}
+323
View File
@@ -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,
);
}
+10
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
enum LayerImportSourceType { geoJson, kml, kmz }
+6
View File
@@ -0,0 +1,6 @@
enum MapEditTool {
none,
point,
line,
polygon,
}
+1
View File
@@ -0,0 +1 @@
enum LayerType { geojson, shapefile, dxf, csv, postgis, wms, track, gnss }
+1
View File
@@ -0,0 +1 @@
enum LayerVisibility { visible, hidden }
+1
View File
@@ -0,0 +1 @@
enum MapMeasureType { none, distance, area }
+1
View File
@@ -0,0 +1 @@
enum MapSurveyMode { browse, measure, stakeout, fieldWalk, track }
+1
View File
@@ -0,0 +1 @@
enum NoteType { point, line, polygon }
+41 -21
View File
@@ -1,35 +1,55 @@
import 'package:nmea/nmea.dart'; import 'package:nmea/nmea.dart';
class Gngga extends TalkerSentence { class Gngga extends TalkerSentence {
static const String id = "GNGGA"; static const String id = "GxGGA";
Gngga({required super.raw}); Gngga({required super.raw});
@override @override
bool get valid => super.valid && fields.length == 15; bool get valid => super.valid && fields.length >= 15;
String get utcOfPositionFix => fields[1]; String get utcOfPositionFix => _field(1);
int get gpsQualityIndicator => int.parse(fields[6]); int get gpsQualityIndicator => _parseInt(_field(6), fallback: 0);
double get latitude => _getCoordinateFromGpsLatitude(fields[2]); String get latitudeDirection => _field(3).toUpperCase();
String get latitudeDirection => fields[3]; double get latitude => _parseLatitude(_field(2), latitudeDirection) ?? 0.0;
double get longitude => _getCoordinateFromGpsLongitude(fields[4]); String get longitudeDirection => _field(5).toUpperCase();
String get longitudeDirection => fields[5]; double get longitude => _parseLongitude(_field(4), longitudeDirection) ?? 0.0;
int get numberOfSvsInUse => int.parse(fields[7]); int get numberOfSvsInUse => _parseInt(_field(7), fallback: 0);
double get hdop => double.parse(fields[8]); double get hdop => _parseDouble(_field(8), fallback: 0.0);
double get altitudeAboveMeanSeaLevel => double.parse(fields[9]); double get altitudeAboveMeanSeaLevel =>
double get geoidSeparation => double.parse(fields[11]); _parseDouble(_field(9), fallback: 0.0);
double get geoidSeparation => _parseDouble(_field(11), fallback: 0.0);
double _getCoordinateFromGpsLongitude(String coordinate) { String _field(int index) {
double result = if (index < 0 || index >= fields.length) return '';
double.parse("${coordinate[0]}${coordinate[1]}${coordinate[2]}") +
double.parse(coordinate.substring(3)) / 60;
return result; return fields[index].trim();
} }
double _getCoordinateFromGpsLatitude(String coordinate) { int _parseInt(String value, {required int fallback}) {
double result = double.parse("${coordinate[0]}${coordinate[1]}") + return int.tryParse(value) ?? fallback;
double.parse(coordinate.substring(2)) / 60; }
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;
} }
} }
+106
View File
@@ -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;
}
}
+29 -10
View File
@@ -1,18 +1,37 @@
import 'package:nmea/nmea.dart'; import 'package:nmea/nmea.dart';
class Gngst extends TalkerSentence { class Gngst extends TalkerSentence {
static const String id = "GNGST"; static const String id = 'GxGST';
Gngst({required super.raw}); Gngst({required super.raw});
@override @override
bool get valid => super.valid && fields.length == 9; bool get valid => super.valid && fields.length >= 9;
String get utcOfPositionFix => fields[1]; String get utcOfPositionFix => _field(1);
double get rms => double.parse(fields[2]);
double get errorEllipseSemiMajorAxis => double.parse(fields[3]); double get rms => _parseDouble(_field(2), fallback: 0.0);
double get errorEllipseSemiMinorAxis => double.parse(fields[4]);
double get errorEllipseOrientation => double.parse(fields[5]); double get errorEllipseSemiMajorAxis =>
double get longitudeError => double.parse(fields[6]); _parseDouble(_field(3), fallback: 0.0);
double get latitudeError => double.parse(fields[7]);
double get heightError => double.parse(fields[8]); 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;
}
} }
+116
View File
@@ -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();
}
}
+57 -24
View File
@@ -1,38 +1,71 @@
import 'package:nmea/nmea.dart'; import 'package:nmea/nmea.dart';
class Gnrmc extends TalkerSentence { class Gnrmc extends TalkerSentence {
static const String id = "GNRMC"; static const String id = 'GxRMC';
Gnrmc({required super.raw}); Gnrmc({required super.raw});
@override @override
bool get valid => super.valid && fields.length == 14; bool get valid => super.valid && fields.length >= 10;
String get messageId => fields[0]; String get messageId => _field(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];
double _getCoordinateFromGpsLongitude(String coordinate) { String get utcOfPositionFix => _field(1);
double result =
double.parse("${coordinate[0]}${coordinate[1]}${coordinate[2]}") +
double.parse(coordinate.substring(3)) / 60;
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 _parseDouble(String value, {required double fallback}) {
double result = double.parse("${coordinate[0]}${coordinate[1]}") + return double.tryParse(value) ?? fallback;
double.parse(coordinate.substring(2)) / 60; }
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
View File
@@ -1,17 +1,104 @@
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:supabase_flutter/supabase_flutter.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/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 { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); 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( await Supabase.initialize(
url: 'https://supa.app-dev.hu', url: dotenv.env['SUPABASE_URL']!,
anonKey: anonKey: dotenv.env['SUPABASE_ANON_KEY']!);
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzExMDYyMDAwLAogICJleHAiOiAxODY4ODI4NDAwCn0.XWtP3eEysZDxXjaHHUZyyhw0n4YZo_xWUMWS5ajBcbI');
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()); runApp(const MyApp());
} }
+11
View File
@@ -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);
// }
+17
View File
@@ -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
// }
// }
+20
View File
@@ -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;
}
+23
View File
@@ -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" : ""}';
// }
+11
View File
@@ -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);
// }
+76
View File
@@ -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,
);
}
+73
View File
@@ -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)';
}
+96
View File
@@ -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(),
);
}
}
+101
View File
@@ -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);
}
}
+81
View File
@@ -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;
}
+183
View File
@@ -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));
}
+70
View File
@@ -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),
);
}
+70
View File
@@ -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),
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
class PointWithDescription { class PointWithDescription {
final int Id; final int pointId;
final DateTime dateTime; final DateTime dateTime;
final String description; final String description;
final double eovX; final double eovX;
@@ -10,7 +10,7 @@ class PointWithDescription {
final double verticalError; final double verticalError;
PointWithDescription( PointWithDescription(
this.Id, this.pointId,
this.dateTime, this.dateTime,
this.description, this.description,
this.eovX, this.eovX,
+112
View File
@@ -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;
}
+225
View File
@@ -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 (090°, 90 = zenit)
final int elevation;
/// Azimut fokban (0359°, 0/360 = É, 90 = K)
final int azimuth;
/// Jelerősség dB-Hz-ben (099, 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';
}
}
}
+251
View File
@@ -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',
);
}
+197
View File
@@ -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:get/get.dart';
import 'package:terepi_seged/pages/home/presentation/controllers/home_controller.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/pages/home/presentation/widgets/big_button_widget.dart';
import 'package:terepi_seged/widgets/app_drawer.dart';
class HomeView extends GetView<HomeViewController> { class HomeView extends GetView<HomeViewController> {
const HomeView({Key? key}) : super(key: key); const HomeView({Key? key}) : super(key: key);
@@ -10,21 +11,7 @@ class HomeView extends GetView<HomeViewController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: Colors.white, color: Colors.white,
child: Scaffold( child: Column(children: [
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();
},
),
),
body: Column(children: [
Column( Column(
children: const [ children: const [
Padding( Padding(
@@ -55,16 +42,44 @@ class HomeView extends GetView<HomeViewController> {
// child: const Text('RTCM teszt')) // 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(
padding: const EdgeInsets.only(top: 10.0), padding: const EdgeInsets.only(top: 10.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
// BigButtonWidget(
// iconData: Icons.flag,
// label: "Kitűzés",
// onPressed: () async {
// Get.toNamed('/map');
// },
// ),
BigButtonWidget( BigButtonWidget(
iconData: Icons.flag, iconData: Icons.flag,
label: "Kitűzés", label: "Bemérés",
onPressed: () async { onPressed: () async {
Get.toNamed('/map'); Get.toNamed('/map_survey');
}, },
), ),
BigButtonWidget( BigButtonWidget(
@@ -110,8 +125,7 @@ class HomeView extends GetView<HomeViewController> {
label: "Üzenetek", label: "Üzenetek",
onPressed: () async { onPressed: () async {
// Get.toNamed("/navigation"); // Get.toNamed("/navigation");
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
.showSnackBar(const SnackBar(
content: Text( content: Text(
"Fejlesztlés alatt", "Fejlesztlés alatt",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@@ -133,8 +147,7 @@ class HomeView extends GetView<HomeViewController> {
label: "Ingatlanok", label: "Ingatlanok",
onPressed: () async { onPressed: () async {
// Get.toNamed('/map_test'); // Get.toNamed('/map_test');
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
.showSnackBar(const SnackBar(
content: Text( content: Text(
"Fejlesztlés alatt", "Fejlesztlés alatt",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@@ -147,15 +160,15 @@ class HomeView extends GetView<HomeViewController> {
iconData: Icons.edit_road, iconData: Icons.edit_road,
label: "Track", label: "Track",
onPressed: () async { onPressed: () async {
// Get.toNamed("/navigation"); Get.toNamed("/tracking");
ScaffoldMessenger.of(context) // ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar( // .showSnackBar(const SnackBar(
content: Text( // content: Text(
"Fejlesztlés alatt", // "Fejlesztlés alatt",
style: TextStyle(fontWeight: FontWeight.bold), // style: TextStyle(fontWeight: FontWeight.bold),
), // ),
backgroundColor: Colors.black54, // backgroundColor: Colors.black54,
)); // ));
}, },
), ),
], ],
@@ -171,6 +184,6 @@ class HomeView extends GetView<HomeViewController> {
) )
], ],
) )
]))); ]));
} }
} }
@@ -9,7 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:flutter_map/flutter_map.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:flutter_map_polywidget/flutter_map_polywidget.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -19,6 +19,9 @@ import 'package:nmea/nmea.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart' import 'package:permission_handler/permission_handler.dart'
as permission_handler; 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/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/eov/eov.dart';
import 'package:terepi_seged/gnss_sentences/gngga.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/gnss_sentences/gnrmc.dart';
import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.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 { class MapViewController extends GetxController {
// String gpsAddress = "E8:31:CD:14:8B:B2"; // String gpsAddress = "E8:31:CD:14:8B:B2";
// String gpsAddress = "98:CD:AC:62:FF:4E"; // String gpsAddress = "98:CD:AC:62:FF:4E";
RxString gpsAddress = "98:CD:AC:62:FF:36".obs; 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"; // String gpsName = "TiGNSS Rover-8BB2";
static const double maxZoomValue = 25.0; static const double maxZoomValue = 25.0;
Rx<bool> gpsIsConnected = false.obs; Rx<bool> gpsIsConnected = false.obs;
@@ -57,6 +62,8 @@ class MapViewController extends GetxController {
DateTime.now().add(const Duration(seconds: -30)); DateTime.now().add(const Duration(seconds: -30));
NumberFormat formatEov = NumberFormat("##0,000.0", "hu-HU"); 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 formatEovForFile = NumberFormat("#####0.0", "hu-HU");
NumberFormat formatWgs84Sec = NumberFormat('00.000', 'hu-HU'); NumberFormat formatWgs84Sec = NumberFormat('00.000', 'hu-HU');
@@ -65,12 +72,15 @@ class MapViewController extends GetxController {
RxDouble gpsLongitude = 0.0.obs; RxDouble gpsLongitude = 0.0.obs;
RxString gpsLongitudeDirection = "".obs; RxString gpsLongitudeDirection = "".obs;
RxDouble gpsAltitude = 0.0.obs; RxDouble gpsAltitude = 0.0.obs;
RxDouble gpsGeoidSeparation = 0.0.obs;
RxInt gpsQuality = 0.obs; RxInt gpsQuality = 0.obs;
RxDouble gpsLatitudeError = 0.0.obs; RxDouble gpsLatitudeError = 0.0.obs;
RxDouble gpsLongitudeError = 0.0.obs; RxDouble gpsLongitudeError = 0.0.obs;
RxDouble gpsAltitudeError = 0.0.obs; RxDouble gpsAltitudeError = 0.0.obs;
Rx<DateTime> gpsDateTime = DateTime(2000).obs; Rx<DateTime> gpsDateTime = DateTime(2000).obs;
Rx<Eov> eov = Eov(0, 0).obs; Rx<Eov> eov = Eov(0, 0).obs;
Rx<double?> eovHeight = 0.0.obs;
RxInt latDegree = 0.obs; RxInt latDegree = 0.obs;
RxInt latMin = 0.obs; RxInt latMin = 0.obs;
RxDouble latSec = 0.0.obs; RxDouble latSec = 0.0.obs;
@@ -92,6 +102,10 @@ class MapViewController extends GetxController {
RxInt pointsToMeasureSelectedValue = (-1).obs; RxInt pointsToMeasureSelectedValue = (-1).obs;
RxDouble distance = 0.0.obs; RxDouble distance = 0.0.obs;
late GeoidGrid geoidGrid;
StreamSubscription<LocationData>? _phoneLocationSub;
final _phoneLocation = Location();
TextEditingController pointIdController = TextEditingController(); TextEditingController pointIdController = TextEditingController();
TextEditingController pointDescriptionController = TextEditingController(); TextEditingController pointDescriptionController = TextEditingController();
TextEditingController gpsHeightController = TextEditingController(); TextEditingController gpsHeightController = TextEditingController();
@@ -103,8 +117,8 @@ class MapViewController extends GetxController {
int pointId = 1; int pointId = 1;
String pointIdPrefix = ""; String pointIdPrefix = "";
String pointIdPostfix = ""; String pointIdPostfix = "";
String ntripUserName = ""; RxString ntripUserName = "".obs;
String ntripPassword = ""; RxString ntripPassword = "".obs;
Rx<bool> pointMeasuringDirectionForward = true.obs; Rx<bool> pointMeasuringDirectionForward = true.obs;
@@ -112,10 +126,10 @@ class MapViewController extends GetxController {
late Directory? directory; late Directory? directory;
late File dataFile; late File dataFile;
late proj4.Projection eovProj, wgsProj;
RxBool mapIsInitialized = false.obs; RxBool mapIsInitialized = false.obs;
GeoJsonParser parser = GeoJsonParser(defaultMarkerColor: Colors.yellow); // GeoJsonParser parser =
// GeoJsonParser(defaultMarkerColor: const Color.fromARGB(255, 85, 34, 49));
final CollectionReference _measuredPoints = final CollectionReference _measuredPoints =
FirebaseFirestore.instance.collection('measuredPoints'); FirebaseFirestore.instance.collection('measuredPoints');
@@ -123,27 +137,46 @@ class MapViewController extends GetxController {
// late SMIBool gpsTrigger; // late SMIBool gpsTrigger;
// late StateMachineController riveGpsIconController; // late StateMachineController riveGpsIconController;
late SharedPreferences prefs;
Rx<bool> isShowPassword = false.obs;
final passwordFieldFocusNode = FocusNode();
late AuthResponse authResponse;
late Session? session;
late User? user;
@override @override
void onInit() async { void onInit() async {
super.onInit(); 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 //Loading, Success, Error handle with 1 line of code
nmeaDecoder nmeaDecoder
..registerTalkerSentence("GGA", (line) => Gngga(raw: line)) ..registerTalkerSentence("GGA", (line) => Gngga(raw: line))
..registerTalkerSentence("RMC", (line) => Gnrmc(raw: line)) ..registerTalkerSentence("RMC", (line) => Gnrmc(raw: line))
..registerTalkerSentence("GST", (line) => Gngst(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(); mapController = MapController();
// riveGpsIconController = RiveUtils.getRiveController(Artboard(), // riveGpsIconController = RiveUtils.getRiveController(Artboard(),
@@ -154,7 +187,7 @@ class MapViewController extends GetxController {
} }
@override @override
void onClose() { void onClose() async {
super.onClose(); super.onClose();
FlutterBluetoothSerial.instance.setPairingRequestHandler(null); FlutterBluetoothSerial.instance.setPairingRequestHandler(null);
if (gpsIsConnected.value) { if (gpsIsConnected.value) {
@@ -162,6 +195,9 @@ class MapViewController extends GetxController {
// connection = null; // connection = null;
print("BluetoothTestController dispose ...."); print("BluetoothTestController dispose ....");
} }
_stopPhoneGps();
pointDescriptionController.dispose(); pointDescriptionController.dispose();
pointIdController.dispose(); pointIdController.dispose();
gpsHeightController.dispose(); gpsHeightController.dispose();
@@ -169,6 +205,10 @@ class MapViewController extends GetxController {
pointPostfixController.dispose(); pointPostfixController.dispose();
ntripUsernameController.dispose(); ntripUsernameController.dispose();
ntripPasswordController.dispose(); ntripPasswordController.dispose();
await Supabase.instance.client
.channel('public:TerepiSeged_Receiver')
.unsubscribe();
} }
@override @override
@@ -200,6 +240,30 @@ class MapViewController extends GetxController {
} else { } else {
print("No external storage permission"); 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(); directory = await getExternalStorageDirectory();
@@ -228,7 +292,7 @@ class MapViewController extends GetxController {
} }
} }
gpsHeightController.text = '1.7'; gpsHeightController.text = '1.8';
} }
void _getInitialLocation() async { void _getInitialLocation() async {
@@ -260,6 +324,57 @@ class MapViewController extends GetxController {
mapController.move(LatLng(currentLatitude.value, currentLongitude.value), mapController.move(LatLng(currentLatitude.value, currentLongitude.value),
currentZoom.value); currentZoom.value);
_updateCurrentLocationMarker(); _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() { void connectToGps() {
@@ -268,6 +383,7 @@ class MapViewController extends GetxController {
connection = value; connection = value;
gpsIsConnected.value = true; gpsIsConnected.value = true;
print("GPS is connected ..."); print("GPS is connected ...");
_stopPhoneGps();
connection.input!.listen(_onDataReceived); connection.input!.listen(_onDataReceived);
}); });
@@ -278,6 +394,7 @@ class MapViewController extends GetxController {
if (gpsIsConnected.value) { if (gpsIsConnected.value) {
connection.close(); connection.close();
gpsIsConnected.value = false; gpsIsConnected.value = false;
_startPhoneGps();
print("GPS is disconnected ...."); print("GPS is disconnected ....");
} }
if (ntripIsConnected.value) { if (ntripIsConnected.value) {
@@ -302,7 +419,9 @@ class MapViewController extends GetxController {
header += "User-Agent: SharpGps iter.dk\r\n"; header += "User-Agent: SharpGps iter.dk\r\n";
header += "Accept: */*\r\nConnection: close\r\n"; header += "Accept: */*\r\nConnection: close\r\n";
// header += "Authorization: Basic ${_toBase64("info@mail.app-dev.hu:")}\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:rtk2go.com:2101\r\n";
header += "Host:gnssnet.hu:2101\r\n"; header += "Host:gnssnet.hu:2101\r\n";
header += "Ntrip-Vesrsion:Ntrip/2.0\r\n"; header += "Ntrip-Vesrsion:Ntrip/2.0\r\n";
@@ -412,17 +531,23 @@ class MapViewController extends GetxController {
gpsLongitude.value = sentence.longitude; gpsLongitude.value = sentence.longitude;
gpsLongitudeDirection.value = sentence.longitudeDirection; gpsLongitudeDirection.value = sentence.longitudeDirection;
gpsAltitude.value = sentence.altitudeAboveMeanSeaLevel; gpsAltitude.value = sentence.altitudeAboveMeanSeaLevel;
gpsGeoidSeparation.value = sentence.geoidSeparation;
gpsQuality.value = sentence.gpsQualityIndicator; gpsQuality.value = sentence.gpsQualityIndicator;
eov.value = ConvertCoordinate.ConvertWgsToEov( eov.value = ConvertCoordinate.ConvertWgsToEov(
gpsLatitude.value, gpsLongitude.value); gpsLatitude.value, gpsLongitude.value);
eovHeight.value = geoidGrid?.toEovHeight(
gpsLatitude.value,
gpsLongitude.value,
gpsAltitude.value,
gpsGeoidSeparation.value);
if (pointsToMeasureSelectedValue.value >= 0) { if (pointsToMeasureSelectedValue.value >= 0) {
double coordX = double coordX =
pointsToMeasure[pointsToMeasureSelectedValue.value].coordX; pointsToMeasure[pointsToMeasureSelectedValue.value].coordX;
double coordY = double coordY =
pointsToMeasure[pointsToMeasureSelectedValue.value].coordY; pointsToMeasure[pointsToMeasureSelectedValue.value].coordY;
var eovCoord = proj4.Point(x: coordX, y: coordY); var wgsCoord =
var wgsCoord = eovProj.transform(wgsProj, eovCoord); CoordConverterService.to.eovToWgsPoint(coordX, coordY);
distance.value = calculateDistance( distance.value = calculateDistance(
LatLng(gpsLatitude.value, gpsLongitude.value), LatLng(gpsLatitude.value, gpsLongitude.value),
LatLng(wgsCoord.y, wgsCoord.x)); LatLng(wgsCoord.y, wgsCoord.x));
@@ -441,7 +566,7 @@ class MapViewController extends GetxController {
(DateTime.now() (DateTime.now()
.difference(lastSendTimeGgaMessage) .difference(lastSendTimeGgaMessage)
.inSeconds >= .inSeconds >=
30)) { 5)) {
sendGgaMessage(lastGgaMessage); sendGgaMessage(lastGgaMessage);
print("Send GGA message: $lastGgaMessage"); print("Send GGA message: $lastGgaMessage");
ggaSenDataPacketNumber.value++; ggaSenDataPacketNumber.value++;
@@ -723,6 +848,31 @@ class MapViewController extends GetxController {
"gpsHeight": gpsHeightController.text "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 (pointMeasuringDirectionForward.isTrue) {
if (pointsToMeasureSelectedValue.value < if (pointsToMeasureSelectedValue.value <
pointsToMeasure.length - 1) { pointsToMeasure.length - 1) {
@@ -802,8 +952,8 @@ class MapViewController extends GetxController {
pointsToMeasure pointsToMeasure
.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY)); .add(PointToMeasure(id: id, coordX: coordX, coordY: coordY));
var eovCoord = proj4.Point(x: coordX, y: coordY); var wgsCoord =
var wgsCoord = eovProj.transform(wgsProj, eovCoord); CoordConverterService.to.eovToWgsPoint(coordX, coordY);
print("Lat -> ${wgsCoord.x}, Long -> ${wgsCoord.y}"); 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) { void pointsToMeasureSelectedValueChanged(int value) {
pointsToMeasureSelectedValue.value = value; pointsToMeasureSelectedValue.value = value;
print('Selected point -> ${pointsToMeasureSelectedValue.value}'); print('Selected point -> ${pointsToMeasureSelectedValue.value}');
@@ -887,4 +1109,101 @@ class MapViewController extends GetxController {
void showAddPointDialog() { void showAddPointDialog() {
onBottomNavigationBarTap(0); 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);
}
} }
+28 -15
View File
@@ -30,7 +30,7 @@ class MapView extends GetView<MapViewController> {
children: [ children: [
Text('Térkép'), Text('Térkép'),
Text( Text(
"Budapest", "",
style: TextStyle(fontSize: 12.0), style: TextStyle(fontSize: 12.0),
) )
], ],
@@ -89,8 +89,27 @@ class MapView extends GetView<MapViewController> {
Padding( Padding(
padding: const EdgeInsets.only(right: 20.0), padding: const EdgeInsets.only(right: 20.0),
child: GestureDetector( child: GestureDetector(
onTap: () => Get.to(() => SettingsDialog(), onTap: () {
transition: Transition.downToUp), 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( child: const Icon(
Icons.more_vert, Icons.more_vert,
size: 26.0, size: 26.0,
@@ -135,7 +154,7 @@ class MapView extends GetView<MapViewController> {
"EovX: ${controller.formatEov.format(controller.eov.value.Y)}"), "EovX: ${controller.formatEov.format(controller.eov.value.Y)}"),
Text( Text(
"EovY: ${controller.formatEov.format(controller.eov.value.X)}"), "EovY: ${controller.formatEov.format(controller.eov.value.X)}"),
Text("Alt: ${controller.gpsAltitude} (m)"), Text("Alt: ${controller.eovHeight.value} (m)"),
const Divider( const Divider(
height: 5.0, height: 5.0,
color: Colors.black, color: Colors.black,
@@ -224,10 +243,11 @@ class MapView extends GetView<MapViewController> {
? FlutterMap( ? FlutterMap(
mapController: controller.mapController, mapController: controller.mapController,
options: MapOptions( options: MapOptions(
center: LatLng(controller.currentLatitude.value, initialCenter: LatLng(
controller.currentLatitude.value,
controller.currentLongitude.value), controller.currentLongitude.value),
maxZoom: 25, maxZoom: 25,
zoom: controller.currentZoom.value, initialZoom: controller.currentZoom.value,
), ),
children: [ children: [
TileLayer( TileLayer(
@@ -243,7 +263,7 @@ class MapView extends GetView<MapViewController> {
), ),
MarkerLayer( MarkerLayer(
markers: controller.currentLocationMarker), markers: controller.currentLocationMarker),
MarkerLayer(markers: controller.parser.markers), // MarkerLayer(markers: controller.parser.markers),
MarkerLayer( MarkerLayer(
markers: controller.pointsToMeasureMarker), markers: controller.pointsToMeasureMarker),
// PolylineLayer( // PolylineLayer(
@@ -438,14 +458,7 @@ class MapView extends GetView<MapViewController> {
FloatingActionButton( FloatingActionButton(
onPressed: () { onPressed: () {
// controller.isMapMoveToCenter(); // controller.isMapMoveToCenter();
// controller.addMeasuredPoint(); controller.showMeasuredPointsTableDialog();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(
"Fejlesztlés alatt",
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.black54,
));
}, },
heroTag: 'Database test', heroTag: 'Database test',
tooltip: 'Pont bemérése', 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>(); final controller = Get.find<MapViewController>();
SettingsDialog({super.key}); SettingsDialog({super.key});
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: ListView( body: ListView(
@@ -18,6 +17,19 @@ class SettingsDialog extends StatelessWidget {
children: [ children: [
IconButton( IconButton(
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(); Get.back();
}, },
icon: const Icon(Icons.close)), icon: const Icon(Icons.close)),
@@ -25,7 +37,22 @@ class SettingsDialog extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
overlayColor: overlayColor:
MaterialStateProperty.all(Colors.transparent)), 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( child: const Text(
'Bezár', 'Bezár',
style: TextStyle(color: Colors.blue, fontSize: 14.0), style: TextStyle(color: Colors.blue, fontSize: 14.0),
@@ -48,12 +75,95 @@ class SettingsDialog extends StatelessWidget {
), ),
), ),
Obx(() => Column(children: [ 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( RadioListTile(
title: Text('TiGNSS Rover-FF4E'), title: Text('TiGNSS Rover-FF4E'),
value: '98:CD:AC:62:FF:4E', value: '98:CD:AC:62:FF:4E',
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
controller.gpsName.value = 'TiGNSS Rover-FF4E';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-FF4E');
}), }),
RadioListTile( RadioListTile(
title: Text('TiGNSS Rover-8BB2'), title: Text('TiGNSS Rover-8BB2'),
@@ -61,7 +171,9 @@ class SettingsDialog extends StatelessWidget {
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
; controller.gpsName.value = 'TiGNSS Rover-8BB2';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-8BB2');
}), }),
RadioListTile( RadioListTile(
title: Text('TiGNSS Rover-FF36'), title: Text('TiGNSS Rover-FF36'),
@@ -69,6 +181,9 @@ class SettingsDialog extends StatelessWidget {
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
controller.gpsName.value = 'TiGNSS Rover-FF36';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-FF36');
}) })
])), ])),
const Padding( const Padding(
@@ -215,26 +330,54 @@ class SettingsDialog extends StatelessWidget {
height: 40, height: 40,
child: TextField( child: TextField(
controller: controller.ntripUsernameController, controller: controller.ntripUsernameController,
decoration: const InputDecoration( enableSuggestions: false,
border: OutlineInputBorder(), 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', labelText: 'Felhasználónév',
icon: Icon(Icons.account_circle_rounded)), prefixIcon: Icon(Icons.account_circle_rounded)),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
SizedBox( Obx(
() => SizedBox(
height: 40, height: 40,
child: TextField( child: TextField(
obscureText: true, keyboardType: TextInputType.visiblePassword,
obscureText: !controller.isShowPassword.value,
focusNode: controller.passwordFieldFocusNode,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
controller: controller.ntripPasswordController, controller: controller.ntripPasswordController,
decoration: const InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
filled: true,
fillColor: Colors.grey.shade300,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)),
labelText: 'Jelszó', labelText: 'Jelszó',
icon: Icon( prefixIcon: Icon(
Icons.lock, 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) 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/services.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:flutter_map/flutter_map.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:flutter_map_polywidget/flutter_map_polywidget.dart';
import 'package:get/get.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/date_time_patterns.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:location/location.dart'; import 'package:location/location.dart';
@@ -20,6 +21,8 @@ import 'package:nmea/nmea.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart' import 'package:permission_handler/permission_handler.dart'
as permission_handler; 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/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/eov/eov.dart';
import 'package:terepi_seged/gnss_sentences/gngga.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_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:proj4dart/proj4dart.dart' as proj4; import 'package:proj4dart/proj4dart.dart' as proj4;
import 'package:shared_preferences/shared_preferences.dart';
class NavigationViewController extends GetxController { class NavigationViewController extends GetxController {
// String gpsAddress = "E8:31:CD:14:8B:B2"; // String gpsAddress = "E8:31:CD:14:8B:B2";
// String gpsAddress = "98:CD:AC:62:FF:4E"; // String gpsAddress = "98:CD:AC:62:FF:4E";
RxString gpsAddress = "98:CD:AC:62:FF:36".obs; 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"; // String gpsName = "TiGNSS Rover-8BB2";
static const double maxZoomValue = 25.0; static const double maxZoomValue = 25.0;
Rx<bool> gpsIsConnected = false.obs; Rx<bool> gpsIsConnected = false.obs;
@@ -118,10 +122,20 @@ class NavigationViewController extends GetxController {
final CollectionReference _vibratorTracker = final CollectionReference _vibratorTracker =
FirebaseFirestore.instance.collection('vibratorTracker'); FirebaseFirestore.instance.collection('vibratorTracker');
DateTime lastGpsDataSaveTime = DateTime(2000, 1, 1, 0, 0, 0); DateTime lastGpsDataSaveTime = DateTime(2000, 1, 1, 0, 0, 0);
int vehicleNumber = 2; RxInt vehicleNumber = 5.obs;
late Location internalGpsLogger; late Location internalGpsLogger;
Location internalGpsLocation = Location(); 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 SMIBool gpsTrigger;
// late StateMachineController riveGpsIconController; // late StateMachineController riveGpsIconController;
@@ -146,13 +160,18 @@ class NavigationViewController extends GetxController {
..registerTalkerSentence("GST", (line) => Gngst(raw: line)); ..registerTalkerSentence("GST", (line) => Gngst(raw: line));
mapController = MapController(); 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(), // riveGpsIconController = RiveUtils.getRiveController(Artboard(),
// stateMachineName: "gps_Interactivity"); // stateMachineName: "gps_Interactivity");
// gpsTrigger = riveGpsIconController.findSMI("active"); // gpsTrigger = riveGpsIconController.findSMI("active");
mapIsInitialized.value = true; mapIsInitialized.value = true;
vehicleNumberController.text = vehicleNumber.toString();
} }
@override @override
@@ -175,7 +194,7 @@ class NavigationViewController extends GetxController {
// internalGpsLogTimer.cancel(); // internalGpsLogTimer.cancel();
// } // }
internalGpsLocationSubscription.cancel(); internalGpsLocationSubscription!.cancel();
} }
@override @override
@@ -184,8 +203,8 @@ class NavigationViewController extends GetxController {
_getInitialLocation(); _getInitialLocation();
String data = await rootBundle.loadString('assets/Files/kozmuvek.geojson'); // String data = await rootBundle.loadString('assets/Files/kozmuvek.geojson');
parser.parseGeoJsonAsString(data); // parser.parseGeoJsonAsString(data);
if (await permission_handler.Permission.storage.isGranted) { if (await permission_handler.Permission.storage.isGranted) {
print("Storage permission is ok ..."); print("Storage permission is ok ...");
@@ -207,6 +226,37 @@ class NavigationViewController extends GetxController {
} else { } else {
print("No external storage permission"); 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(); directory = await getExternalStorageDirectory();
@@ -256,7 +306,7 @@ class NavigationViewController extends GetxController {
await internalGpsLocation.changeSettings(interval: 10000); await internalGpsLocation.changeSettings(interval: 10000);
internalGpsLocationSubscription = internalGpsLocationSubscription =
internalGpsLogger.onLocationChanged.listen((LocationData value) { internalGpsLogger.onLocationChanged.listen((LocationData value) async {
double? longitude; double? longitude;
double? latitude; double? latitude;
double? accuracy; double? accuracy;
@@ -276,16 +326,29 @@ class NavigationViewController extends GetxController {
internalGpsFile.writeAsString( internalGpsFile.writeAsString(
"$vehicleNumber;$time;$latitude;$longitude;$altitude;$accuracy\r\n"); "$vehicleNumber;$time;$latitude;$longitude;$altitude;$accuracy\r\n");
_vibratorTracker.add({ // _vibratorTracker.add({
"gpsType": 0, // "gpsType": 0,
"vibratorNumber": vehicleNumber, // "vibratorNumber": vehicleNumber.value,
"gpsDateTime": time, // "gpsDateTime": time,
"latitude": latitude, // "latitude": latitude,
"longitude": longitude, // "longitude": longitude,
"altitude": altitude, // "altitude": altitude,
"horizontalError": accuracy, // "horizontalError": accuracy,
"speed": speed, // "speed": speed,
"createdAt": DateTime.timestamp() // "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 && if (DateTime.now().difference(lastGpsDataSaveTime).inSeconds >= 10 &&
gpsQuality.value > 0) { gpsQuality.value > 0) {
lastGpsDataSaveTime = DateTime.now();
_savePositionToDatabase(); _savePositionToDatabase();
print("Save to databases: ${lastGpsDataSaveTime}");
} }
} }
void _savePositionToDatabase() { void _savePositionToDatabase() async {
_vibratorTracker.add({ _vibratorTracker.add({
"gpsType": 1, "gpsType": 1,
"vibratorNumber": vehicleNumber, "vibratorNumber": vehicleNumber.value,
"gpsQuality": gpsQuality.value, "gpsQuality": gpsQuality.value,
"gpsDateTime": gpsDateTime.value, "gpsDateTime": gpsDateTime.value,
"latitude": gpsLatitude.value, "latitude": gpsLatitude.value,
@@ -601,7 +667,25 @@ class NavigationViewController extends GetxController {
"antennaHeight": double.parse(gpsHeightController.text), "antennaHeight": double.parse(gpsHeightController.text),
"createdAt": DateTime.timestamp() "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}) { String getGpsQualityIndicator({required int quality}) {
@@ -896,15 +980,15 @@ class NavigationViewController extends GetxController {
width: 15.0, width: 15.0,
height: 15.0, height: 15.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.purple, color: Colors.yellow,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(width: 1.0, color: Colors.black)), border: Border.all(width: 1.0, color: Colors.black)),
))); )));
pointsToMeasureLabel.add(PolyWidget( pointsToMeasureLabel.add(PolyWidget(
center: LatLng(wgsCoord.y + 0.0000075, wgsCoord.x + 0.0000075), center: LatLng(wgsCoord.y + 0.0000275, wgsCoord.x + 0.0000275),
widthInMeters: 3, widthInMeters: 28,
heightInMeters: 3, heightInMeters: 28,
// constraints: const BoxConstraints( // constraints: const BoxConstraints(
// minWidth: 250, // minWidth: 250,
// maxWidth: 350, // 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) { void pointsToMeasureSelectedValueChanged(int value) {
pointsToMeasureSelectedValue.value = value; pointsToMeasureSelectedValue.value = value;
print('Selected point -> ${pointsToMeasureSelectedValue.value}'); print('Selected point -> ${pointsToMeasureSelectedValue.value}');
@@ -964,4 +1066,24 @@ class NavigationViewController extends GetxController {
void addMeasuredPoint() { void addMeasuredPoint() {
// _measuredPoints.add({"id": 4001, "latitude": 46.3455, "longitude": 19.652}); // _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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Navigáció'), Text('Navigáció'),
Text( // Text(
"Mérés: Budapest-4", // "Mérés: Zalaegerszeg",
style: TextStyle(fontSize: 12.0), // style: TextStyle(fontSize: 12.0),
) // )
], ],
), ),
actions: [ actions: [
@@ -84,6 +84,16 @@ class NavigationView extends GetView<NavigationViewController> {
const Icon(Icons.file_open, size: 26.0, color: Colors.blue), 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(
padding: const EdgeInsets.only(right: 20.0), padding: const EdgeInsets.only(right: 20.0),
child: GestureDetector( child: GestureDetector(
@@ -222,10 +232,11 @@ class NavigationView extends GetView<NavigationViewController> {
? FlutterMap( ? FlutterMap(
mapController: controller.mapController, mapController: controller.mapController,
options: MapOptions( options: MapOptions(
center: LatLng(controller.currentLatitude.value, initialCenter: LatLng(
controller.currentLatitude.value,
controller.currentLongitude.value), controller.currentLongitude.value),
maxZoom: 25, maxZoom: 25,
zoom: controller.currentZoom.value, initialZoom: controller.currentZoom.value,
), ),
children: [ children: [
TileLayer( TileLayer(
@@ -241,13 +252,14 @@ class NavigationView extends GetView<NavigationViewController> {
), ),
MarkerLayer( MarkerLayer(
markers: controller.currentLocationMarker), markers: controller.currentLocationMarker),
MarkerLayer(markers: controller.parser.markers), // MarkerLayer(markers: controller.parser.markers),
PolylineLayer(polylines: controller.pathLayer),
MarkerLayer( MarkerLayer(
markers: controller.pointsToMeasureMarker), markers: controller.pointsToMeasureMarker),
// PolylineLayer( PolylineLayer(
// polylines: controller.parser.polylines), polylines: controller.parser.polylines),
PolyWidgetLayer( PolyWidgetLayer(
polyWidgets: controller.pointsToMeasureLabel) polyWidgets: controller.pointsToMeasureLabel),
], ],
) )
: const Center(child: CircularProgressIndicator()), : const Center(child: CircularProgressIndicator()),
@@ -1,13 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.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'; import 'package:terepi_seged/pages/navigation/presentation/controllers/navigation_controller.dart';
class SettingsDialog extends StatelessWidget { class SettingsDialog extends StatelessWidget {
final controller = Get.find<NavigationViewController>(); final controller = Get.find<NavigationViewController>();
SettingsDialog({super.key}); SettingsDialog({super.key});
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: ListView( body: ListView(
@@ -19,8 +19,24 @@ class SettingsDialog extends StatelessWidget {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
controller.vehicleNumber = 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); int.parse(controller.vehicleNumberController.text);
controller.saveVehicleNumber(
int.parse(controller.vehicleNumberController.text));
}
Get.back(); Get.back();
}, },
icon: const Icon(Icons.close)), icon: const Icon(Icons.close)),
@@ -28,7 +44,29 @@ class SettingsDialog extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
overlayColor: overlayColor:
MaterialStateProperty.all(Colors.transparent)), 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( child: const Text(
'Bezár', 'Bezár',
style: TextStyle(color: Colors.blue, fontSize: 14.0), style: TextStyle(color: Colors.blue, fontSize: 14.0),
@@ -51,12 +89,95 @@ class SettingsDialog extends StatelessWidget {
), ),
), ),
Obx(() => Column(children: [ 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( RadioListTile(
title: Text('TiGNSS Rover-FF4E'), title: Text('TiGNSS Rover-FF4E'),
value: '98:CD:AC:62:FF:4E', value: '98:CD:AC:62:FF:4E',
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
controller.gpsName.value = 'TiGNSS Rover-FF4E';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-FF4E');
}), }),
RadioListTile( RadioListTile(
title: Text('TiGNSS Rover-8BB2'), title: Text('TiGNSS Rover-8BB2'),
@@ -64,7 +185,9 @@ class SettingsDialog extends StatelessWidget {
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
; controller.gpsName.value = 'TiGNSS Rover-8BB2';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-8BB2');
}), }),
RadioListTile( RadioListTile(
title: Text('TiGNSS Rover-FF36'), title: Text('TiGNSS Rover-FF36'),
@@ -72,6 +195,9 @@ class SettingsDialog extends StatelessWidget {
groupValue: controller.gpsAddress.value, groupValue: controller.gpsAddress.value,
onChanged: (value) { onChanged: (value) {
controller.gpsAddress.value = value!; controller.gpsAddress.value = value!;
controller.gpsName.value = 'TiGNSS Rover-FF36';
controller.saveGpsAddress(value);
controller.saveGpsName('TiGNSS Rover-FF36');
}) })
])), ])),
const Padding( const Padding(
@@ -173,15 +299,7 @@ class SettingsDialog extends StatelessWidget {
)), )),
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Divider(
height: 5,
),
),
const SizedBox(height: 4),
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0), padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Text( 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; StreamSubscription? socketStreamSubscription;
RxString message = ''.obs; RxString message = ''.obs;
final GoogleSignIn googleSignIn = // final GoogleSignIn googleSignIn =
GoogleSignIn(scopes: ['https://www.googleapis.com/auth/drive.appdata']); // GoogleSignIn(scopes: ['https://www.googleapis.com/auth/drive.appdata']);
late GoogleSignInAccount? googleSignInAccount; // late GoogleSignInAccount? googleSignInAccount;
@override @override
void onInit() { void onInit() {
@@ -31,13 +31,13 @@ class SocketTestController extends GetxController {
} }
void googleLogIn() async { void googleLogIn() async {
googleSignInAccount = await googleSignIn.signIn(); // googleSignInAccount = await googleSignIn.signIn();
final GoogleSignInAuthentication googleSignInAuthentication = // final GoogleSignInAuthentication googleSignInAuthentication =
await googleSignInAccount!.authentication; // await googleSignInAccount!.authentication;
print("AccessToken: ${googleSignInAuthentication.accessToken}"); // print("AccessToken: ${googleSignInAuthentication.accessToken}");
print("IdToken: ${googleSignInAuthentication.idToken}"); // print("IdToken: ${googleSignInAuthentication.idToken}");
} }
void connect() async { 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 { void _departStartPage() async {
await Future.delayed(const Duration(seconds: 5)); 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),
),
],
),
),
),
),
);
}
}
+34 -1
View File
@@ -1,11 +1,16 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:terepi_seged/pages/bleutooth/bindings/bluetooth_bindings.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/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/bindings/home_bindings.dart';
import 'package:terepi_seged/pages/home/presentation/views/home_view.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/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_add_point_dialog.dart';
import 'package:terepi_seged/pages/map/presentation/views/map_view.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/bindings/measured_data_bindings.dart';
import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart'; import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart';
import 'package:terepi_seged/pages/navigation/bindings/navigation_bindings.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/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/bindings/rtcm_test_bindings.dart';
import 'package:terepi_seged/pages/rtcm_test/presentation/views/rtcm_test_view.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/bindings/socket_test_bindings.dart';
import 'package:terepi_seged/pages/socket_test/presentation/views/socket_test_view.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/bindings/start_page_bindings.dart';
import 'package:terepi_seged/pages/start/presentation/views/start_page.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/bindings/map_test_bindings.dart';
import '../pages/map_test/presentation/views/map_test_view.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'; part 'app_routes.dart';
// ignore: avoid_classes_with_only_static_members // ignore: avoid_classes_with_only_static_members
@@ -40,6 +52,11 @@ class AppPages {
name: Routes.STARTPAGE, name: Routes.STARTPAGE,
page: () => const StartPage(), page: () => const StartPage(),
binding: StartPageBinding()), binding: StartPageBinding()),
GetPage(
name: Routes.SHELL,
page: () => const ShellView(),
binding: ShellBinding(),
),
GetPage( GetPage(
name: Routes.MAP, page: () => const MapView(), binding: MapBinding()), name: Routes.MAP, page: () => const MapView(), binding: MapBinding()),
GetPage( GetPage(
@@ -73,6 +90,22 @@ class AppPages {
GetPage( GetPage(
name: Routes.MEASUREDDATA, name: Routes.MEASUREDDATA,
binding: MeasuredDataBinding(), 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())
]; ];
} }
+10
View File
@@ -14,6 +14,16 @@ abstract class Routes {
static const MAPTEST = '/map_test'; static const MAPTEST = '/map_test';
static const PROPERTYLIST = '/property_list'; static const PROPERTYLIST = '/property_list';
static const MEASUREDDATA = '/measured_data'; 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 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
+239
View File
@@ -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();
}
}
}
+157
View File
@@ -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;
}
}
}
+76
View File
@@ -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