diff --git a/android/build.gradle b/android/build.gradle index cff2187..a0c36a3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '2.0.21' + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() diff --git a/android/settings.gradle b/android/settings.gradle index 7a27933..9d3994e 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -23,7 +23,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.3.2' apply false + id "com.android.application" version '8.6.0' apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/doc/MNemo DMP File Description.md b/doc/MNemo DMP File Description.md index 70e7501..605f303 100644 --- a/doc/MNemo DMP File Description.md +++ b/doc/MNemo DMP File Description.md @@ -1,108 +1,252 @@ -# Mnemo v2 DMP File Format (File Version 5): # +# Mnemo v2 DMP File Format # -**[No File Header]** +## File Version 5 (Standard Format) +**[No File Header]** **[Per Section Header]** _(13 Bytes)_ Fileversion; ( 5 for firmware >2.6.0) - + 68; - + 89; - + 101; - + Year; - + Month; - + Day ; - + Hour ; - + Minute ; - + NameChar1; - + NameChar2; - + NameChar3; - + Direction ; ( 0=IN,1=OUT) **[Per Shot]** _(35 Bytes)_ 57; - + + 67; + + 77; + + Typeshot ; (0=CSA,1=CSB,2=STD,3=EOL) + + HeadingIN LSB; + + HeadingIN MSB; + + HeadingOUT LSB; + + HeadingOUT MSB; + + Length LSB; + + Length MSB; + + DepthIN LSB; + + DepthIN MSB; + + DepthOUT LSB; + + DepthOUT MSB; + + PitchIN LSB; + + PitchIN MSB; + + PitchOUT LSB; + + PitchOUT MSB; + + Left LSB; + + Left MSB; + + Right LSB; + + Right MSB; + + Up LSB; + + Up MSB; + + Down LSB; + + Down MSB; + + Temperature LSB; + + Temperature MSB; + + Hour; + + Minute; + + Second; + + MarkerIdx; + + 95; + + 25; + + 35; + +**[Section Termination]** _(35 Bytes)_ + + 57;67;77;3; [28 times 0;]95;25;35; + +--- + +## File Version 6 (Dry Caving Device with Lidar Support) + +**IMPORTANT**: File Version 6 is identical to Version 5 for all common fields, with only optional Lidar data added. + +**[No File Header]** + +**[Per Section Header]** _(13 Bytes - Same as V5)_ + + Fileversion; ( 6 for dry caving devices with Lidar support) + + 68; + + 89; + + 101; + + Year; + + Month; + + Day ; + + Hour ; + + Minute ; + + NameChar1; + + NameChar2; + + NameChar3; + + Direction ; ( 0=IN,1=OUT) + +**[Per Shot]** _(35 Bytes - Same as V5 + Optional Lidar Data)_ + + 57; + 67; - + 77; - + Typeshot ; (0=CSA,1=CSB,2=STD,3=EOL) - + HeadingIN LSB; - + HeadingIN MSB; - + HeadingOUT LSB; - + HeadingOUT MSB; - + Length LSB; - + Length MSB; - + DepthIN LSB; - + DepthIN MSB; - + DepthOUT LSB; - + DepthOUT MSB; - + PitchIN LSB; - + PitchIN MSB; - + PitchOUT LSB; - + PitchOUT MSB; - + Left LSB; - + Left MSB; - + Right LSB; - + Right MSB; - + Up LSB; - + Up MSB; - + Down LSB; - + Down MSB; - + Temperature LSB; - + Temperature MSB; - + Hour; - + Minute; - + Second; - + MarkerIdx; - + 95; - + 25; - + 35; +**[Optional Lidar Data]** _(Variable Length)_ + + 32; # VOLSTART_VALA + + 33; # VOLSTART_VALB + + 34; # VOLSTART_VALC + + DataLength LSB; + DataLength MSB; + + # Repeat for DataLength/6 triplet entries: + YAW LSB; # (uint16_t) 1/100th of degree to magnetic north (0 = north) + YAW MSB; + + PITCH LSB; # (int16_t) 1/100th of degree + PITCH MSB; + + DISTANCE LSB; # (uint16_t) cm + DISTANCE MSB; + **[Section Termination]** _(35 Bytes)_ 57;67;77;3; [28 times 0;]95;25;35; + +## Format Notes + +- **File Version 5**: Standard format for wet caving devices +- **File Version 6**: Enhanced format for dry caving devices with optional Lidar data +- **Byte Order**: Both V5 and V6 use little-endian (LSB first) for all 16-bit values +- **Compatibility**: V6 is fully backward compatible with V5 for all common fields +- **Lidar Data**: Only present in V6 format when device captures 3D point cloud data. +- **Magic Bytes**: Used for data validation and format detection +- **Data Length**: Specifies the total bytes of Lidar data (must be divisible by 6) diff --git a/lib/excelexport.dart b/lib/excelexport.dart index 52cb5b4..b49704f 100644 --- a/lib/excelexport.dart +++ b/lib/excelexport.dart @@ -4,24 +4,10 @@ import 'package:intl/intl.dart'; import 'package:excel/excel.dart'; import 'models/models.dart'; + void writeHeaderOnSheet(Sheet sheet, int rowNumber) { var ls = [ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P" + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S" ]; int index = 0; @@ -70,87 +56,85 @@ void writeHeaderOnSheet(Sheet sheet, int rowNumber) { cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); cell.value = TextCellValue("Marker"); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Yaw"); + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Pitch"); + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Distance"); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); cell.value = TextCellValue("Comments"); } -void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { + +int writeRowOnSheet(Section section, Shot data, Sheet sheet, int startRowNumber) { var ls = [ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P" + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S" ]; - int index = 0; final cellStyleWithNumberFormatForNumber = CellStyle( numberFormat: NumFormat.standard_0, ); - var cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + int currentRow = startRowNumber; + + // Write the main shot data row + int index = 0; + var cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = TextCellValue(data.typeShot.name); - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.getCalculatedLength()); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.depthIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.depthOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.headingIn / 10.0); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(data.headingIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.headingOut / 10.0); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(data.headingOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.pitchIn / 10.0); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(data.pitchIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.pitchOut / 10.0); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(data.pitchOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.left); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.right); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.up); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.down); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.temperature / 10.0); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(data.temperature); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = TextCellValue(DateTime( section.dateSurvey.year, section.dateSurvey.month, @@ -160,16 +144,50 @@ void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { data.sec) .toIso8601String()); - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = IntCellValue(data.markerIndex); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = TextCellValue(data.usesCalculatedLength() - ? "Length calculated from depth change and inclination (original measurement was insufficient)" + // Leave Lidar columns empty for main shot row + index += 3; // Skip Lidar Yaw, Pitch, Distance columns + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = TextCellValue(data.usesCalculatedLength() + ? "Length calculated from depth change and inclination (original measurement was insufficient)" : ""); + + currentRow++; + + // Write Lidar data rows if available + if (data.hasLidarData() && data.lidarData != null) { + for (final lidarPoint in data.lidarData!.points) { + // Clear all columns for Lidar-only rows + index = 0; + + // Skip all main shot data columns (15 columns) + index += 15; + + // Write Lidar data + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.yaw); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.pitch); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.distance); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + currentRow++; + } + } + + return currentRow; } + void writeTitleOnSheet(Sheet sheet, Section s, UnitType unitType) { var cell = sheet.cell(CellIndex.indexByString("A1")); cell.value = TextCellValue(s.getName()); @@ -212,10 +230,17 @@ void exportAsExcel(SectionList sectionList, File file, UnitType unitType) { int rownum = 5; + // Check if this section has Lidar data and add a note if so + bool hasLidarData = element.getShots().any((shot) => shot.hasLidarData()); + if (hasLidarData) { + var cell = sheet.cell(CellIndex.indexByString("A4")); + cell.value = TextCellValue("Survey includes Lidar data"); + } + writeHeaderOnSheet(sheet, rownum++); - element.getShots().forEach((data) { - writeRowOnSheet(element, data, sheet, rownum++); - }); + for (final data in element.getShots()) { + rownum = writeRowOnSheet(element, data, sheet, rownum); + } } excel.delete("Sheet1"); diff --git a/lib/mapsurvey.dart b/lib/mapsurvey.dart index 42cb859..266c3bc 100644 --- a/lib/mapsurvey.dart +++ b/lib/mapsurvey.dart @@ -35,22 +35,27 @@ class MapSurvey { isProblematicShot.add(shot.hasProblematicLength()); // Calculate horizontal distance using corrected length - final depthChange = (shot.depthOut - shot.depthIn).abs(); + final depthChange = shot.depthOut - shot.depthIn; // Preserve sign for direction + final absDepthChange = depthChange.abs(); double factecr; - - if (lengthToUse <= depthChange) { + + if (lengthToUse <= absDepthChange) { // For vertical or near-vertical shots, use minimal horizontal distance factecr = 0.1; } else { - factecr = sqrt(pow(lengthToUse, 2) - pow(depthChange, 2)); + factecr = sqrt(pow(lengthToUse, 2) - pow(absDepthChange, 2)); } - + + // Get the best vertical displacement (depth sensor or calculated from angles) + final verticalDisplacement = shot.getBestVerticalDisplacement(); + final adjustedHeading = (90-(shot.headingIn + shot.headingOut)/2); + points.add(Point3d( points[i].x + - factecr * sin(-shot.headingOut / 3600.0 * 2.0 * pi), + factecr * cos(adjustedHeading * pi / 180.0), points[i].y + - factecr * cos(shot.headingOut / 3600.0 * 2.0 * pi), - max(shot.depthOut, i < section.shots.length-1 ? section.shots[i+1].depthIn : shot.depthOut))); + factecr * sin(adjustedHeading * pi / 180.0), + points[i].z + verticalDisplacement)); // Use calculated vertical displacement } } diff --git a/lib/models/lidar_data.dart b/lib/models/lidar_data.dart new file mode 100644 index 0000000..bcc9cec --- /dev/null +++ b/lib/models/lidar_data.dart @@ -0,0 +1,31 @@ +/// Represents a single Lidar measurement point +class LidarPoint { + final double yaw; // degrees (processed measurement) + final double pitch; // degrees (processed measurement) + final double distance; // m (processed measurement) + + const LidarPoint({ + required this.yaw, + required this.pitch, + required this.distance, + }); + + /// Get yaw in degrees (for compatibility) + double get yawDegrees => yaw; + + /// Get pitch in degrees (for compatibility) + double get pitchDegrees => pitch; +} + +/// Represents Lidar data collection for a shot +class LidarData { + final List points; + + const LidarData({required this.points}); + + /// Check if this contains valid Lidar data + bool get hasData => points.isNotEmpty; + + /// Get number of Lidar points + int get pointCount => points.length; +} \ No newline at end of file diff --git a/lib/models/models.dart b/lib/models/models.dart index f7b2b11..f2dc2d0 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -3,4 +3,5 @@ export 'enums.dart'; export 'shot.dart'; export 'section.dart'; export 'section_list.dart'; -export 'survey_quality.dart'; \ No newline at end of file +export 'survey_quality.dart'; +export 'lidar_data.dart'; \ No newline at end of file diff --git a/lib/models/shot.dart b/lib/models/shot.dart index ded3353..83da683 100644 --- a/lib/models/shot.dart +++ b/lib/models/shot.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'enums.dart'; +import 'lidar_data.dart'; /// Represents a single shot measurement in a cave survey class Shot { @@ -9,11 +10,11 @@ class Shot { double depthIn; double depthOut; - // Compass readings (in degrees * 10) - int headingIn; - int headingOut; - int pitchIn; - int pitchOut; + // Compass readings (in degrees) + double headingIn; + double headingOut; + double pitchIn; + double pitchOut; // LRUD measurements (Left, Right, Up, Down) double left; @@ -22,31 +23,35 @@ class Shot { double down; // Additional metadata - int temperature; + double temperature; int hr; // Hour - int min; // Minute + int min; // Minute int sec; // Second int markerIndex; + // Optional Lidar data (V6 format and later) + LidarData? lidarData; + /// Default constructor Shot({ this.typeShot = TypeShot.std, this.length = 0.0, - this.headingIn = 0, - this.headingOut = 0, - this.pitchIn = 0, - this.pitchOut = 0, + this.headingIn = 0.0, + this.headingOut = 0.0, + this.pitchIn = 0.0, + this.pitchOut = 0.0, this.depthIn = 0.0, this.depthOut = 0.0, this.left = 0.0, this.right = 0.0, this.up = 0.0, this.down = 0.0, - this.temperature = 0, + this.temperature = 0.0, this.hr = 0, this.min = 0, this.sec = 0, this.markerIndex = 0, + this.lidarData, }); /// Creates a zero-valued shot @@ -65,17 +70,17 @@ class Shot { double getDepthOut() => depthOut; void setDepthOut(double newDepthOut) => depthOut = newDepthOut; - int getHeadingIn() => headingIn; - void setHeadingIn(int newHeadingIn) => headingIn = newHeadingIn; - - int getHeadingOut() => headingOut; - void setHeadingOut(int newHeadingOut) => headingOut = newHeadingOut; - - int getPitchIn() => pitchIn; - void setPitchIn(int newPitchIn) => pitchIn = newPitchIn; - - int getPitchOut() => pitchOut; - void setPitchOut(int newPitchOut) => pitchOut = newPitchOut; + double getHeadingIn() => headingIn; + void setHeadingIn(double newHeadingIn) => headingIn = newHeadingIn; + + double getHeadingOut() => headingOut; + void setHeadingOut(double newHeadingOut) => headingOut = newHeadingOut; + + double getPitchIn() => pitchIn; + void setPitchIn(double newPitchIn) => pitchIn = newPitchIn; + + double getPitchOut() => pitchOut; + void setPitchOut(double newPitchOut) => pitchOut = newPitchOut; double getLeft() => left; void setLeft(double newLeft) => left = newLeft; @@ -89,8 +94,8 @@ class Shot { double getDown() => down; void setDown(double newDown) => down = newDown; - int getTemperature() => temperature; - void setTemperature(int newTemperature) => temperature = newTemperature; + double getTemperature() => temperature; + void setTemperature(double newTemperature) => temperature = newTemperature; int getHr() => hr; void setHr(int newHr) => hr = newHr; @@ -118,8 +123,8 @@ class Shot { if (!hasProblematicLength()) return length; final depthChange = getDepthChange(); - // Use average inclination from pitchIn and pitchOut (in degrees/10) - final avgPitch = (pitchIn + pitchOut) / 2.0 / 10.0; // Convert to degrees + // Use average inclination from pitchIn and pitchOut (already in degrees) + final avgPitch = (pitchIn + pitchOut) / 2.0; final radians = avgPitch * pi / 180.0; // Convert to radians // If inclination is near 90 degrees (vertical), use depth change as length @@ -133,4 +138,34 @@ class Shot { /// Check if this shot uses calculated length (for display purposes) bool usesCalculatedLength() => hasProblematicLength(); + + /// Calculate true vertical displacement using pitch angles + /// This is used when depth sensor is unreliable (e.g., above water scenarios) + double getCalculatedVerticalDisplacement() { + final lengthToUse = hasProblematicLength() ? getCalculatedLength() : length; + final avgPitch = (pitchIn + pitchOut) / 2.0; + final pitchRadians = avgPitch * pi / 180.0; + + return lengthToUse * sin(pitchRadians); + } + + /// Get the best available vertical displacement + /// Uses depth sensor data when reliable, otherwise calculates from angles + double getBestVerticalDisplacement() { + final depthChange = depthOut - depthIn; // Preserve sign + final absDepthChange = depthChange.abs(); + final lengthToUse = hasProblematicLength() ? getCalculatedLength() : length; + + // If depth sensor reading is very small but we have significant length and pitch, + // prefer calculated displacement from angles + if (absDepthChange < 0.1 && lengthToUse > 0.5) { + return getCalculatedVerticalDisplacement(); + } else { + // Use depth sensor reading + return depthChange; + } + } + + /// Check if this shot has Lidar data + bool hasLidarData() => lidarData?.hasData ?? false; } \ No newline at end of file diff --git a/lib/services/data_processing_service.dart b/lib/services/data_processing_service.dart index 1fda33a..5d5b076 100644 --- a/lib/services/data_processing_service.dart +++ b/lib/services/data_processing_service.dart @@ -17,9 +17,14 @@ class DataProcessingService { static const int _shotEndValueB = 25; static const int _shotEndValueC = 35; + // V6 format Lidar data constants + static const int _lidarStartValueA = 32; + static const int _lidarStartValueB = 33; + static const int _lidarStartValueC = 34; + /// Process raw binary transfer buffer into survey sections Future processTransferBuffer( - List transferBuffer, + List transferBuffer, UnitType unitType ) async { try { @@ -30,26 +35,26 @@ class DataProcessingService { final sections =
[]; int cursor = 0; bool brokenSegmentDetected = false; - + final conversionFactor = unitType == UnitType.metric ? 1.0 : 3.28084; while (cursor < transferBuffer.length - 2) { final sectionResult = await _processSection( - transferBuffer, - cursor, + transferBuffer, + cursor, conversionFactor ); - + if (sectionResult.section != null) { sections.add(sectionResult.section!); } - + cursor = sectionResult.newCursor; - + if (sectionResult.brokenSegment) { brokenSegmentDetected = true; } - + if (sectionResult.shouldStop) { break; } @@ -61,7 +66,7 @@ class DataProcessingService { } return DataProcessingResult.success( - sections, + sections, brokenSegmentDetected: brokenSegmentDetected ); } catch (e) { @@ -88,7 +93,7 @@ class DataProcessingService { // Find file version int fileVersion = 0; - while (fileVersion != 2 && fileVersion != 3 && fileVersion != 4 && fileVersion != 5) { + while (fileVersion != 2 && fileVersion != 3 && fileVersion != 4 && fileVersion != 5 && fileVersion != 6) { if (cursor >= transferBuffer.length) { return _SectionProcessingResult(null, cursor, false, true); } @@ -203,9 +208,6 @@ class DataProcessingService { int cursor = startCursor; final shot = Shot.zero(); - if (kDebugMode) { - debugPrint("DataProcessingService: Processing shot at cursor $cursor"); - } // Validate shot start magic bytes for version 5+ if (fileVersion >= 5) { @@ -246,31 +248,72 @@ class DataProcessingService { shot.typeShot = TypeShot.values[typeShot]; + // This is handling broken sample file I got. Might not be needed in final version. + if (kDebugMode && shot.typeShot == TypeShot.eoc) { + debugPrint("DataProcessingService: EOC shot detected, cursor at $cursor"); + if (cursor + 20 < transferBuffer.length) { + debugPrint("DataProcessingService: Next 20 bytes after type: ${transferBuffer.sublist(cursor, cursor + 20)}"); + } + } + + // For EOC shots, only skip the 9 zero bytes and don't read shot fields + if (shot.typeShot == TypeShot.eoc) { + cursor += 9; // Skip the 9 zero bytes in EOC data + if (kDebugMode) { + debugPrint("DataProcessingService: EOC shot - skipped 9 zero bytes, cursor at $cursor"); + } + + // Some V6 files incorrectly have Lidar data after EOC shots - skip it + if (fileVersion >= 6) { + if (cursor + 2 < transferBuffer.length && + transferBuffer[cursor] == _lidarStartValueA && + transferBuffer[cursor + 1] == _lidarStartValueB && + transferBuffer[cursor + 2] == _lidarStartValueC) { + if (kDebugMode) { + debugPrint("DataProcessingService: Skipping invalid Lidar data after EOC shot at position $cursor"); + } + cursor += 3; // Skip Lidar magic bytes + + if (cursor + 1 < transferBuffer.length) { + final lidarLength = _readIntFromBuffer(transferBuffer, cursor); + cursor += 2; + cursor += lidarLength; // Skip Lidar data + + if (kDebugMode) { + debugPrint("DataProcessingService: Skipped $lidarLength bytes of Lidar data, cursor now at $cursor"); + } + } + } + } + + return _ShotProcessingResult(shot, cursor, false); + } + // Read shot data (need at least 16 bytes for basic shot data) if (cursor + 15 >= transferBuffer.length) { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - shot.headingIn = _readIntFromBuffer(transferBuffer, cursor); + shot.headingIn = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - - shot.headingOut = _readIntFromBuffer(transferBuffer, cursor); + + shot.headingOut = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - + shot.length = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + shot.depthIn = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + shot.depthOut = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - - shot.pitchIn = _readIntFromBuffer(transferBuffer, cursor); + + shot.pitchIn = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - - shot.pitchOut = _readIntFromBuffer(transferBuffer, cursor); + + shot.pitchOut = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; if (kDebugMode) { @@ -281,13 +324,14 @@ class DataProcessingService { "pitch=${shot.pitchIn}/${shot.pitchOut}"); } + // Read LRUD data for version 4+ if (fileVersion >= 4) { if (cursor + 7 >= transferBuffer.length) { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + shot.left = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; shot.right = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; @@ -296,7 +340,7 @@ class DataProcessingService { cursor += 2; shot.down = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + if (kDebugMode) { debugPrint("DataProcessingService: LRUD: ${shot.left} ${shot.right} ${shot.up} ${shot.down}"); } @@ -308,13 +352,14 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - - shot.temperature = _readIntFromBuffer(transferBuffer, cursor); + + shot.temperature = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; + shot.hr = _readByteFromBuffer(transferBuffer, cursor++); shot.min = _readByteFromBuffer(transferBuffer, cursor++); shot.sec = _readByteFromBuffer(transferBuffer, cursor++); - + if (kDebugMode) { debugPrint("DataProcessingService: Temperature=${shot.temperature}, " "Time=${shot.hr}:${shot.min}:${shot.sec}"); @@ -326,7 +371,7 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + shot.markerIndex = _readByteFromBuffer(transferBuffer, cursor++); // Validate shot end magic bytes for version 5+ @@ -335,21 +380,32 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + final checkByteA = _readByteFromBuffer(transferBuffer, cursor++); final checkByteB = _readByteFromBuffer(transferBuffer, cursor++); final checkByteC = _readByteFromBuffer(transferBuffer, cursor++); - + if (checkByteA != _shotEndValueA || checkByteB != _shotEndValueB || checkByteC != _shotEndValueC) { if (kDebugMode) { debugPrint("DataProcessingService: Invalid shot end magic bytes"); - } + } return _ShotProcessingResult(shot, cursor - 3, true); } } + // Process optional Lidar data for V6 format (but not for EOC shots) + if (fileVersion >= 6 && shot.typeShot != TypeShot.eoc) { + final lidarResult = await _processLidarData(transferBuffer, cursor, conversionFactor); + shot.lidarData = lidarResult.lidarData; + cursor = lidarResult.newCursor; + + if (lidarResult.brokenSegment) { + return _ShotProcessingResult(shot, cursor, true); + } + } + return _ShotProcessingResult(shot, cursor, false); } @@ -363,18 +419,177 @@ class DataProcessingService { /// Read a 16-bit integer from the buffer (little endian) int _readIntFromBuffer(List buffer, int address) { - if (address < 0 || address + 1 >= buffer.length) { - return 0; + + if (address + 1 >= buffer.length) return 0; + + final bytes = Uint8List.fromList([buffer[address], buffer[address + 1]]); + final byteData = ByteData.sublistView(bytes); + return byteData.getInt16(0); + + } + + /// Read a 16-bit integer from the buffer (inverted endian) + int _readInvIntFromBuffer(List buffer, int address) { + if (address + 1 >= buffer.length) return 0; + + final bytes = Uint8List.fromList([buffer[address + 1], buffer[address]]); + final byteData = ByteData.sublistView(bytes); + return byteData.getInt16(0); + } + + int _readUInt16FromBuffer(List buffer, int address) { + if (address + 1 >= buffer.length) return 0; + + // notice the order of the bytes is reversed for uint16 + final bytes = Uint8List.fromList([buffer[address + 1 ], buffer[address]]); + final byteData = ByteData.sublistView(bytes); + return byteData.getUint16(0); + } + + + /// Process optional Lidar data from V6 format + Future<_LidarProcessingResult> _processLidarData( + List transferBuffer, + int startCursor, + double conversionFactor, + ) async { + int cursor = startCursor; + + // Check if there's enough space for Lidar header (3 magic bytes + 2 length bytes) + if (cursor + 4 >= transferBuffer.length) { + if (kDebugMode) { + debugPrint("DataProcessingService: No space for Lidar header, skipping"); + } + return _LidarProcessingResult(null, cursor, false); } - - try { - final bytes = Uint8List.fromList([buffer[address], buffer[address + 1]]); - final byteData = ByteData.sublistView(bytes); - return byteData.getInt16(0); - } catch (e) { - return 0; + + // Check for Lidar start magic bytes + final checkByteA = _readByteFromBuffer(transferBuffer, cursor); + final checkByteB = _readByteFromBuffer(transferBuffer, cursor + 1); + final checkByteC = _readByteFromBuffer(transferBuffer, cursor + 2); + + if (checkByteA != _lidarStartValueA || + checkByteB != _lidarStartValueB || + checkByteC != _lidarStartValueC) { + if (kDebugMode) { + debugPrint("DataProcessingService: No Lidar magic bytes found, skipping"); + } + return _LidarProcessingResult(null, cursor, false); + } + + cursor += 3; // Skip magic bytes + + // Read data length + final dataLength = _readIntFromBuffer(transferBuffer, cursor); + cursor += 2; + + // Validate data length and buffer space + if (dataLength == 0 || cursor + dataLength > transferBuffer.length) { + if (kDebugMode) { + debugPrint("DataProcessingService: Invalid Lidar data length or insufficient buffer"); + } + return _LidarProcessingResult(null, cursor, true); + } + + // Each Lidar point is 6 bytes (2 bytes each for YAW, PITCH, DISTANCE) + if (dataLength % 6 != 0) { + if (kDebugMode) { + debugPrint("DataProcessingService: Invalid Lidar data length (not divisible by 6)"); + } + return _LidarProcessingResult(null, cursor + dataLength, true); + } + + final pointCount = dataLength ~/ 6; + final lidarPoints = []; + + // Track statistics for debug output + double minYaw = 0, maxYaw = 0; + double minPitch = 0, maxPitch = 0; + double minDistance = 0, maxDistance = 0; + bool firstPoint = true; + + // Read each Lidar point + for (int i = 0; i < pointCount; i++) { + + final yaw = _readUInt16FromBuffer(transferBuffer, cursor) / 100.0; + cursor += 2; + + final pitch = _readInvIntFromBuffer(transferBuffer, cursor) / 100.0; + cursor += 2; + + final distance = _readUInt16FromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; + cursor += 2; + + // Update statistics + if (kDebugMode) { + + if (firstPoint) { + minYaw = maxYaw = yaw; + minPitch = maxPitch = pitch; + minDistance = maxDistance = distance; + firstPoint = false; + } else { + if (yaw < minYaw) minYaw = yaw; + if (yaw > maxYaw) maxYaw = yaw; + if (pitch < minPitch) minPitch = pitch; + if (pitch > maxPitch) maxPitch = pitch; + if (distance < minDistance) minDistance = distance; + if (distance > maxDistance) maxDistance = distance; + } + } + + lidarPoints.add(LidarPoint( + yaw: yaw, + pitch: pitch, + distance: distance, + )); + } + + final lidarData = LidarData(points: lidarPoints); + + if (kDebugMode && lidarPoints.isNotEmpty) { + // Normalize angle ranges for better representation + final (normalizedMinYaw, normalizedMaxYaw) = _normalizeAngleRange(minYaw, maxYaw); + final (normalizedMinPitch, normalizedMaxPitch) = _normalizeAngleRange(minPitch, maxPitch); + + debugPrint("DataProcessingService: Lidar shot statistics - ${lidarPoints.length} points: " + "Yaw(${normalizedMinYaw.toStringAsFixed(1)}° - ${normalizedMaxYaw.toStringAsFixed(1)}°), " + "Pitch(${normalizedMinPitch.toStringAsFixed(1)}° - ${normalizedMaxPitch.toStringAsFixed(1)}°), " + "Distance(${minDistance}m-${maxDistance}m)"); + } + + return _LidarProcessingResult(lidarData, cursor, false); + } + + /// Normalize angle range to show the most logical representation + /// For example: -5° to +5° instead of 355° to 5° + /// Returns a tuple of (minAngle, maxAngle) in degrees, always with min <= max + (double, double) _normalizeAngleRange(double minAngleDegrees, double maxAngleDegrees) { + // Normalize angles to 0-360 range + double normalizedMin = minAngleDegrees % 360.0; + double normalizedMax = maxAngleDegrees % 360.0; + + if (normalizedMin < 0) normalizedMin += 360.0; + if (normalizedMax < 0) normalizedMax += 360.0; + + // Calculate direct span (clockwise from min to max) + final double directSpan = normalizedMax >= normalizedMin ? + normalizedMax - normalizedMin : + normalizedMin - normalizedMax + 360.0; + + // If direct span is <= 180°, use it; otherwise use boundary-crossing representation + if (directSpan <= 180.0) { + return normalizedMin <= normalizedMax ? + (normalizedMin, normalizedMax) : + (normalizedMax, normalizedMin); + } else { + // Use boundary-crossing representation (shorter path across 0°) + return normalizedMax >= normalizedMin ? + (normalizedMin, normalizedMax - 360.0) : + (normalizedMin - 360.0, normalizedMax); } } + } /// Result of data processing operation @@ -420,4 +635,13 @@ class _ShotProcessingResult { final bool brokenSegment; _ShotProcessingResult(this.shot, this.newCursor, this.brokenSegment); +} + +/// Internal result for Lidar data processing +class _LidarProcessingResult { + final LidarData? lidarData; + final int newCursor; + final bool brokenSegment; + + _LidarProcessingResult(this.lidarData, this.newCursor, this.brokenSegment); } \ No newline at end of file diff --git a/lib/services/lidar_coordinate_calculator.dart b/lib/services/lidar_coordinate_calculator.dart new file mode 100644 index 0000000..860e418 --- /dev/null +++ b/lib/services/lidar_coordinate_calculator.dart @@ -0,0 +1,93 @@ +import 'dart:math'; +import '../mapsurvey.dart'; +import '../models/models.dart'; + +/// Utility class for calculating Lidar global coordinates on-demand +class LidarCoordinateCalculator { + /// Calculate global coordinates for a Lidar point given shot position + static Point3d calculateGlobalCoordinates( + LidarPoint point, + double shotX, + double shotY, + double shotZ, + ) { + // Convert angles to radians + final yawRadians = point.yaw * pi / 180.0; + final pitchRadians = point.pitch * pi / 180.0; + final distanceMeters = point.distance; + + // Calculate global coordinates relative to shot position + // X: East-West (positive = East) - sin(yaw) where yaw=0 is north + // Y: North-South (positive = North) - cos(yaw) where yaw=0 is north + // Z: Vertical (positive = up) + final globalX = shotX + distanceMeters * cos(pitchRadians) * sin(yawRadians); + final globalY = shotY + distanceMeters * cos(pitchRadians) * cos(yawRadians); + final globalZ = shotZ + distanceMeters * sin(pitchRadians); + + return Point3d(globalX, globalY, globalZ); + } + + /// Calculate shot positions for a section (replicates MapSurvey.buildMap logic) + static List calculateShotPositions(Section section) { + final points = []; + final start = Point3d(0, 0, section.shots.first.depthIn); + points.add(start); + + for (int i = 0; i < section.shots.length; i++) { + final shot = section.shots[i]; + + // Use calculated length if shot is problematic, otherwise use original length + final lengthToUse = shot.hasProblematicLength() ? shot.getCalculatedLength() : shot.length; + + // Calculate horizontal distance using corrected length + final depthChange = shot.depthOut - shot.depthIn; // Preserve sign for direction + final absDepthChange = depthChange.abs(); + double horizontalDistance; + + if (lengthToUse <= absDepthChange) { + // For vertical or near-vertical shots, use minimal horizontal distance + horizontalDistance = 0.1; + } else { + horizontalDistance = sqrt(pow(lengthToUse, 2) - pow(absDepthChange, 2)); + } + + // Get the best vertical displacement (depth sensor or calculated from angles) + final verticalDisplacement = shot.getBestVerticalDisplacement(); + + points.add(Point3d( + points[i].x + horizontalDistance * sin(-shot.headingOut * pi / 180.0), + points[i].y + horizontalDistance * cos(shot.headingOut * pi / 180.0), + points[i].z + verticalDisplacement // Use calculated vertical displacement + )); + } + + return points; + } + + /// Get all Lidar points with global coordinates for a section + static List getAllLidarGlobalPoints(Section section) { + final shotPositions = calculateShotPositions(section); + final allLidarPoints = []; + + for (int shotIndex = 0; shotIndex < section.shots.length - 1; shotIndex++) { + final shot = section.shots[shotIndex]; + if (!shot.hasLidarData() || shotIndex + 1 >= shotPositions.length) continue; + + final lidarData = shot.lidarData!; + // Lidar measurements are taken at the arriving point (end) of the shot + final shotPosition = shotPositions[shotIndex + 1]; + + for (final lidarPoint in lidarData.points) { + final globalPoint = calculateGlobalCoordinates( + lidarPoint, + shotPosition.x, + shotPosition.y, + shotPosition.z, + ); + allLidarPoints.add(globalPoint); + } + } + + return allLidarPoints; + } +} \ No newline at end of file diff --git a/lib/services/survey_quality_service.dart b/lib/services/survey_quality_service.dart index b8f4c1a..0cc4362 100644 --- a/lib/services/survey_quality_service.dart +++ b/lib/services/survey_quality_service.dart @@ -51,12 +51,12 @@ class SurveyQualityService { for (int i = 0; i < section.shots.length - 1; i++) { // Exclude EOC shot final shot = section.shots[i]; - // Check heading consistency (angles are in degrees * 10) - final headingDiff = ((shot.headingOut - shot.headingIn).abs() / 10.0) % 360; + // Check heading consistency (angles are already in degrees) + final headingDiff = (shot.headingOut - shot.headingIn).abs() % 360; final normalizedHeadingDiff = headingDiff > 180 ? 360 - headingDiff : headingDiff; // Check pitch consistency - final pitchDiff = (shot.pitchOut - shot.pitchIn).abs() / 10.0; + final pitchDiff = (shot.pitchOut - shot.pitchIn).abs(); // Track maximum discrepancies maxHeadingDiff = max(maxHeadingDiff, normalizedHeadingDiff); @@ -158,8 +158,8 @@ class SurveyQualityService { // For valid shots, calculate length from inclination and depth change final depthChange = shot.getDepthChange(); if (depthChange > 0 && shot.length > 0) { - // Use average inclination from pitchIn and pitchOut (in degrees/10) - final avgPitch = (shot.pitchIn + shot.pitchOut) / 2.0 / 10.0; // Convert to degrees + // Use average inclination from pitchIn and pitchOut (already in degrees) + final avgPitch = (shot.pitchIn + shot.pitchOut) / 2.0; final radians = avgPitch * pi / 180.0; // Convert to radians double calculatedLength; diff --git a/lib/shotexport.dart b/lib/shotexport.dart index 0b64584..5ba2034 100644 --- a/lib/shotexport.dart +++ b/lib/shotexport.dart @@ -17,6 +17,246 @@ mixin ShotExport { String get extension; + /// Check if a section contains Lidar data (indicating dry cave survey) + bool hasDryCaveSurvey(Section section) { + return section.getShots().any((shot) => shot.hasLidarData()); + } + + /// Generate instrument configuration for survey type + void writeInstrumentConfig( + StringBuffer contents, + bool isDryCave, + String prefix, // '' for Therion, '*' for Survex + ) { + if (isDryCave) { + contents.write(newLine('${prefix}instrument compass "Jedeye"')); + contents.write(newLine('${prefix}instrument clino "Jedeye"')); + contents.write(newLine('${prefix}instrument tape "Jedeye"')); + } else { + contents.write(newLine('${prefix}instrument compass "MNemo V2"')); + contents.write(newLine('${prefix}instrument depth "MNemo V2"')); + contents.write(newLine('${prefix}instrument tape "MNemo V2"')); + } + contents.write('\n'); + + contents.write(newLine('${prefix}sd compass 1.5 degrees')); + contents.write(newLine('${prefix}sd tape 0.086 metres')); + if (!isDryCave) { + contents.write(newLine('${prefix}sd depth 0.1 metres')); + } + contents.write('\n'); + + if (!isDryCave) { + contents.write(newLine('${prefix}calibrate depth 0 -1')); + contents.write('\n'); + } + } + + /// Generate units configuration for survey type + void writeUnitsConfig( + StringBuffer contents, + bool isDryCave, + UnitType unitType, + String prefix, // '' for Therion, '*' for Survex + ) { + if (isDryCave) { + // Dry cave: no depth measurements + if (unitType == UnitType.metric) { + contents.write(newLine('${prefix}units tape metres')); + } else { + contents.write(newLine('${prefix}units tape feet')); + } + contents.write(newLine('${prefix}units clino deg')); + } else { + // Underwater survey: include depth + if (unitType == UnitType.metric) { + contents.write(newLine('${prefix}units tape depth metres')); + } else { + contents.write(newLine('${prefix}units tape depth feet')); + } + } + contents.write('\n'); + } + + /// Generate data format configuration for survey type + void writeDataConfig( + StringBuffer contents, + bool isDryCave, + String prefix, // '' for Therion, '*' for Survex + String commentPrefix, // '#' for Therion, ';' for Survex + ) { + if (isDryCave) { + // Dry cave data format + final dryDataFormat = [ + '${prefix}data', + 'normal', + 'from', + 'to', + 'tape', + 'compass', + 'backcompass', + 'clino', + 'backclino', + 'ignoreall' + ].join(' '); + + final dryHeaderFormat = [ + '$commentPrefix From', + 'To', + 'Length', + 'AzIn', + '180-AzOut', + 'PitchIn', + 'PitchOut', + 'AzMean', + 'AzOut', + 'AzDelta' + ].join('\t'); + + contents.write(newLine(dryDataFormat)); + contents.write(newLine(dryHeaderFormat)); + } else { + // Underwater data format + final underwaterDataFormat = [ + '${prefix}data', + 'diving', + 'from', + 'to', + 'tape', + 'compass', + 'backcompass', + 'fromdepth', + 'todepth', + 'ignoreall' + ].join(' '); + + final underwaterHeaderFormat = [ + '$commentPrefix From', + 'To', + 'Length', + 'AzIn', + '180-AzOut', + 'DepIn', + 'DepOut', + 'AzMean', + 'AzOut', + 'AzDelta', + 'PitchIn', + 'PitchOut' + ].join('\t'); + + contents.write(newLine(underwaterDataFormat)); + contents.write(newLine(underwaterHeaderFormat)); + } + contents.write('\n'); + } + + /// Format shot data line for export + String formatShotDataLine( + ExportShot exportShot, + bool isDryCave, + int paddingWidth, + ) { + final String fromStation = exportShot.from.padLeft(paddingWidth, '0'); + final String toStation = exportShot.to.padLeft(paddingWidth, '0'); + + if (isDryCave) { + return [ + fromStation, + toStation, + exportShot.length.toStringAsFixed(2), + exportShot.azimuthIn.toStringAsFixed(1), + exportShot.azimuthOut180.toStringAsFixed(1), + exportShot.pitchIn.toStringAsFixed(1), + exportShot.pitchOut.toStringAsFixed(1), + exportShot.azimuthMean.toStringAsFixed(1), + exportShot.azimuthOut.toStringAsFixed(1), + exportShot.azimuthDelta.toStringAsFixed(1), + ].join('\t'); + } else { + return [ + fromStation, + toStation, + exportShot.length.toStringAsFixed(2), + exportShot.azimuthIn.toStringAsFixed(1), + exportShot.azimuthOut180.toStringAsFixed(1), + exportShot.depthIn.toStringAsFixed(2), + exportShot.depthOut.toStringAsFixed(2), + exportShot.azimuthMean.toStringAsFixed(1), + exportShot.azimuthOut.toStringAsFixed(1), + exportShot.azimuthDelta.toStringAsFixed(1), + exportShot.pitchIn.toStringAsFixed(1), + exportShot.pitchOut.toStringAsFixed(1), + ].join('\t'); + } + } + + /// Process LRUD and Lidar data for an export shot + /// Returns a map with 'lrud' and 'lidar' StringBuffer contents + Map processLRUDAndLidarData( + ExportShot exportShot, + bool isDryCave, + int paddingWidth, + String commentPrefix, + String Function(String) stationNameFormatter, + ) { + final Map result = { + 'lrud': StringBuffer(), + 'lidar': StringBuffer(), + }; + + if (exportShot.lrudShots.isEmpty) return result; + + final String stationName = stationNameFormatter(exportShot.to); + + // Separate regular LRUD from Lidar data + final List regularLrudShots = exportShot.lrudShots + .where((shot) => shot.direction != LRUDDirection.lidar) + .toList(); + final List lidarShots = exportShot.lrudShots + .where((shot) => shot.direction == LRUDDirection.lidar) + .toList(); + + // Process regular LRUD measurements + if (regularLrudShots.isNotEmpty) { + result['lrud']!.write(newLine('$commentPrefix LRUD for station $stationName')); + for (LRUDShot lrudShot in regularLrudShots) { + result['lrud']!.write(newLine( + '$commentPrefix ${enumToStringWithoutClassName(lrudShot.direction.toString())}')); + + final lrudDataLine = [ + stationName, + '-', + lrudShot.length.toStringAsFixed(2), + lrudShot.azimuth.toStringAsFixed(1), + lrudShot.clino.toStringAsFixed(1) + ].join('\t'); + + result['lrud']!.write(newLine(lrudDataLine)); + } + result['lrud']!.write('\n'); + } + + // Process Lidar measurements + if (lidarShots.isNotEmpty && isDryCave) { + result['lidar']!.write(newLine('$commentPrefix Lidar measurements from station $stationName')); + for (LRUDShot lidarShot in lidarShots) { + final lidarDataLine = [ + stationName, + '-', + lidarShot.length.toStringAsFixed(2), + lidarShot.azimuth.toStringAsFixed(1), + lidarShot.clino.toStringAsFixed(1) + ].join('\t'); + + result['lidar']!.write(newLine(lidarDataLine)); + } + result['lidar']!.write('\n'); + } + + return result; + } + double getAzimuthMean(double az1, double az2) { // Convert degrees to radians final double az1Rad = deg2rad(az1); @@ -116,14 +356,14 @@ mixin ShotExport { } final double length = shot.getCalculatedLength(); - final double azimuthIn = (shot.getHeadingIn().toDouble()) / 10.0; - final double azimuthOut = (shot.getHeadingOut().toDouble()) / 10.0; + final double azimuthIn = shot.getHeadingIn(); + final double azimuthOut = shot.getHeadingOut(); final double azimuthMean = getAzimuthMean(azimuthIn, azimuthOut); final double azimuthDelta = getAzimuthDelta(azimuthIn, azimuthOut); final List azimuthComments = getAzimuthComment(azimuthMean, azimuthDelta, azimuthIn, azimuthOut); - final double pitchIn = (shot.getPitchIn().toDouble()) / 10.0; - final double pitchOut = (shot.getPitchOut().toDouble()) / 10.0; + final double pitchIn = shot.getPitchIn(); + final double pitchOut = shot.getPitchOut(); final double depthIn = shot.getDepthIn(); final double depthOut = shot.getDepthOut(); @@ -139,12 +379,13 @@ mixin ShotExport { depthOut: depthOut, azimuthMean: azimuthMean, azimuthDelta: azimuthDelta, - lurdLeft: shot.getLeft(), - lurdRight: shot.getRight(), - lurdUp: shot.getUp(), - lurdDown: shot.getDown(), + lrudLeft: shot.getLeft(), + lrudRight: shot.getRight(), + lrudUp: shot.getUp(), + lrudDown: shot.getDown(), azimuthComments: azimuthComments, - isCalculatedLength: shot.usesCalculatedLength()); + isCalculatedLength: shot.usesCalculatedLength(), + lidarData: shot.lidarData); svxShots.add(svxShot); @@ -189,7 +430,7 @@ class ExportShot { double depthOut; double azimuthMean; double azimuthDelta; - late List lurdShots; + late List lrudShots; List azimuthComments; bool isCalculatedLength; @@ -205,55 +446,70 @@ class ExportShot { required this.depthOut, required this.azimuthMean, required this.azimuthDelta, - required double lurdLeft, - required double lurdRight, - required double lurdUp, - required double lurdDown, + required double lrudLeft, + required double lrudRight, + required double lrudUp, + required double lrudDown, required this.azimuthComments, required this.isCalculatedLength, + LidarData? lidarData, }) { azimuthOut180 = (azimuthOut + 180) % 360; - lurdShots = []; - if (!almostEqual(0.0, lurdLeft)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.left, - length: lurdLeft, + lrudShots = []; + if (!almostEqual(0.0, lrudLeft)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.left, + length: lrudLeft, azimuth: _addAngles(azimuthMean, -90.0), clino: 0.0, ), ); } - if (!almostEqual(0.0, lurdRight)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.right, - length: lurdRight, + if (!almostEqual(0.0, lrudRight)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.right, + length: lrudRight, azimuth: _addAngles(azimuthMean, 90.0), clino: 0.0, ), ); } - if (!almostEqual(0.0, lurdUp)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.up, - length: lurdUp, + if (!almostEqual(0.0, lrudUp)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.up, + length: lrudUp, azimuth: 0.0, clino: 90.0, ), ); } - if (!almostEqual(0.0, lurdDown)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.down, - length: lurdDown, + if (!almostEqual(0.0, lrudDown)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.down, + length: lrudDown, azimuth: 0.0, clino: -90.0, ), ); } + + // Process Lidar data as LRUD shots + if (lidarData != null && lidarData.hasData) { + for (final point in lidarData.points) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.lidar, + length: point.distance, + azimuth: point.yaw, + clino: point.pitch, + ), + ); + } + } } double _addAngles(double angle, double delta) { @@ -279,13 +535,13 @@ String enumToStringWithoutClassName(dynamic enumValue) { return enumValue.toString().split('.').last; } -class LURDShot { - LURDDirection direction; +class LRUDShot { + LRUDDirection direction; double length; double azimuth; double clino; - LURDShot({ + LRUDShot({ required this.direction, required this.length, required this.azimuth, @@ -293,4 +549,4 @@ class LURDShot { }); } -enum LURDDirection { left, right, up, down } +enum LRUDDirection { left, right, up, down, lidar } diff --git a/lib/survexporter.dart b/lib/survexporter.dart index 8480c69..5a13d2b 100644 --- a/lib/survexporter.dart +++ b/lib/survexporter.dart @@ -22,7 +22,7 @@ class SurvexExporter with ShotExport { Future getContents(Section section, ExportShots exportShots, String surveyName, UnitType unitType) async { StringBuffer contents = StringBuffer(await headerComments()); - StringBuffer lurdContents = StringBuffer(); + StringBuffer lrudContents = StringBuffer(); contents.write(newLine('*begin $surveyName')); @@ -51,33 +51,15 @@ class SurvexExporter with ShotExport { contents.write(newLine('*require 1.2.21')); contents.write('\n'); - contents.write(newLine('*instrument compass "MNemo V2"')); - contents.write(newLine('*instrument depth "MNemo V2"')); - contents.write(newLine('*instrument tape "MNemo V2"')); - contents.write('\n'); - - contents.write(newLine('*sd compass 1.5 degrees')); - contents.write(newLine('*sd depth 0.1 metres')); - contents.write(newLine('*sd tape 0.086 metres')); - contents.write('\n'); + final bool isDryCave = hasDryCaveSurvey(section); - contents.write(newLine('*calibrate depth 0 -1')); - contents.write('\n'); + // Write configuration using shared methods + writeInstrumentConfig(contents, isDryCave, '*'); + writeUnitsConfig(contents, isDryCave, unitType, '*'); + writeDataConfig(contents, isDryCave, '*', ';'); - // Unit handling - if (unitType == UnitType.metric) { - contents.write(newLine('*units tape depth metres')); - } else { - contents.write(newLine('*units tape depth feet')); - } - contents.write('\n'); - - // Main topo data - contents.write(newLine( - '*data diving from to tape compass backcompass fromdepth todepth ignoreall')); - contents.write(newLine( - '; From\tTo\tLength\tAzIn\t180-AzOut\tDepIn\tDepOut\tAzIn\tAzOut\tAzDelta\tPitchIn\tPitchOut')); - contents.write('\n'); + // Calculate padding width for station names based on total number of stations + final int paddingWidth = (exportShots.shots.length + 1).toString().length; bool firstLine = true; for (ExportShot exportShot in exportShots.shots) { @@ -89,36 +71,35 @@ class SurvexExporter with ShotExport { for (var comment in exportShot.azimuthComments) { contents.write(newLine('; $comment')); } - + if (exportShot.isCalculatedLength) { contents.write(newLine('; Length calculated from depth change and inclination (original measurement was insufficient)')); } } // Formatting the measurement line - contents.write(newLine( - '${exportShot.from}\t${exportShot.to}\t${exportShot.length.toStringAsFixed(2)}\t${exportShot.azimuthIn.toStringAsFixed(1)}\t${exportShot.azimuthOut180.toStringAsFixed(1)}\t${exportShot.depthIn.toStringAsFixed(2)}\t${exportShot.depthOut.toStringAsFixed(2)}\t${exportShot.azimuthMean.toStringAsFixed(1)}\t${exportShot.azimuthOut.toStringAsFixed(1)}\t${exportShot.azimuthDelta.toStringAsFixed(1)}\t${exportShot.pitchIn.toStringAsFixed(1)}\t${exportShot.pitchOut.toStringAsFixed(1)}')); - - if (exportShot.lurdShots.isNotEmpty) { - lurdContents.write(newLine('; LURD for station ${exportShot.to}')); - for (LURDShot lurdShot in exportShot.lurdShots) { - lurdContents.write(newLine( - '; ${enumToStringWithoutClassName(lurdShot.direction.toString())}')); - lurdContents.write(newLine( - '${exportShot.to}\t-\t${lurdShot.length.toStringAsFixed(2)}\t${lurdShot.azimuth.toStringAsFixed(1)}\t${lurdShot.clino.toStringAsFixed(1)}')); - } - lurdContents.write('\n'); - } + contents.write(newLine(formatShotDataLine(exportShot, isDryCave, paddingWidth))); + + final Map lrudData = processLRUDAndLidarData( + exportShot, + isDryCave, + paddingWidth, + ';', + (stationTo) => stationTo.padLeft(paddingWidth, '0'), + ); + + lrudContents.write(lrudData['lrud']!.toString()); + lrudContents.write(lrudData['lidar']!.toString()); firstLine = false; } contents.write('\n'); - if (lurdContents.isNotEmpty) { - contents.write(newLine('; LURD measurements')); + if (lrudContents.isNotEmpty) { + contents.write(newLine('; LRUD measurements')); contents.write(newLine('*data normal from to tape compass clino')); contents.write('\n'); - contents.write(lurdContents.toString()); + contents.write(lrudContents.toString()); } // Finalizing the survey contents diff --git a/lib/thexporter.dart b/lib/thexporter.dart index 7f2e4df..cc97227 100644 --- a/lib/thexporter.dart +++ b/lib/thexporter.dart @@ -5,11 +5,12 @@ class THExporter with ShotExport { @override String get extension => '.th'; + @override Future getContents(Section section, ExportShots exportShots, String surveyName, UnitType unitType) async { StringBuffer contents = StringBuffer(newLine('encoding UTF-8')); - StringBuffer lurdContents = StringBuffer(); + StringBuffer lrudContents = StringBuffer(); contents.write('\n'); @@ -33,35 +34,18 @@ class THExporter with ShotExport { contents.write(newLine('#explo-team ""')); contents.write('\n'); - contents.write(newLine('instrument compass "MNemo V2"')); - contents.write(newLine('instrument depth "MNemo V2"')); - contents.write(newLine('instrument tape "MNemo V2"')); - contents.write('\n'); + final bool isDryCave = hasDryCaveSurvey(section); - contents.write(newLine('sd compass 1.5 degrees')); - contents.write(newLine('sd depth 0.1 metres')); - contents.write(newLine('sd tape 0.086 metres')); - contents.write('\n'); + // Write configuration using shared methods + writeInstrumentConfig(contents, isDryCave, ''); + writeUnitsConfig(contents, isDryCave, unitType, ''); + writeDataConfig(contents, isDryCave, '', '#'); - contents.write(newLine('calibrate depth 0 -1')); - contents.write('\n'); + increasePrefix(); - // Unit handling - if (unitType == UnitType.metric) { - contents.write(newLine('units tape depth metres')); - } else { - contents.write(newLine('units tape depth feet')); - } - contents.write('\n'); + // Calculate padding width for station names based on total number of stations + final int paddingWidth = isDryCave ? (exportShots.shots.length + 1).toString().length : 0; - // Main topo data - contents.write(newLine( - 'data diving from to tape compass backcompass fromdepth todepth ignoreall')); - contents.write(newLine( - '# From\tTo\tLength\tAzIn\t180-AzOut\tDepIn\tDepOut\tAzMean\tAzOut\tAzDelta\tPitchIn\tPitchOut')); - contents.write('\n'); - - increasePrefix(); bool firstLine = true; for (ExportShot exportShot in exportShots.shots) { if (exportShot.azimuthComments.isNotEmpty || exportShot.isCalculatedLength) { @@ -79,31 +63,31 @@ class THExporter with ShotExport { } // Formatting the measurement line - contents.write(newLine( - '${exportShot.from}\t${exportShot.to}\t${exportShot.length.toStringAsFixed(2)}\t${exportShot.azimuthIn.toStringAsFixed(1)}\t${exportShot.azimuthOut180.toStringAsFixed(1)}\t${exportShot.depthIn.toStringAsFixed(2)}\t${exportShot.depthOut.toStringAsFixed(2)}\t${exportShot.azimuthMean.toStringAsFixed(1)}\t${exportShot.azimuthOut.toStringAsFixed(1)}\t${exportShot.azimuthDelta.toStringAsFixed(1)}\t${exportShot.pitchIn.toStringAsFixed(1)}\t${exportShot.pitchOut.toStringAsFixed(1)}')); - - if (exportShot.lurdShots.isNotEmpty) { - lurdContents.write(newLine('# LURD for station ${exportShot.to}')); - for (LURDShot lurdShot in exportShot.lurdShots) { - lurdContents.write(newLine( - '# ${enumToStringWithoutClassName(lurdShot.direction.toString())}')); - lurdContents.write(newLine( - '${exportShot.to}\t-\t${lurdShot.length.toStringAsFixed(2)}\t${lurdShot.azimuth.toStringAsFixed(1)}\t${lurdShot.clino.toStringAsFixed(1)}')); - } - lurdContents.write('\n'); - } + contents.write(newLine(formatShotDataLine(exportShot, isDryCave, paddingWidth))); + + final Map lrudData = processLRUDAndLidarData( + exportShot, + isDryCave, + paddingWidth, + '#', + (stationTo) => stationTo.padLeft(paddingWidth, '0'), + ); + + lrudContents.write(lrudData['lrud']!.toString()); + lrudContents.write(lrudData['lidar']!.toString()); firstLine = false; } contents.write('\n'); - if (lurdContents.isNotEmpty) { - contents.write(newLine('# LURD measurements')); + if (lrudContents.isNotEmpty) { + contents.write(newLine('# LRUD measurements')); contents.write(newLine('data normal from to tape compass clino')); contents.write('\n'); - contents.write(lurdContents.toString()); + contents.write(lrudContents.toString()); } + decreasePrefix(); decreasePrefix(); diff --git a/pubspec.yaml b/pubspec.yaml index a990c75..71917e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: intl: null loading_animation_widget: null network_info_plus: ^6.1.0 - package_info_plus: null + package_info_plus: ^8.0.0 path_provider: null shared_preferences: ^2.2.2 slugify: null