| | |
| | | private static final Color HANDHELD_BOUNDARY_BORDER = new Color(51, 102, 204, 220); |
| | | private static final Color HANDHELD_BOUNDARY_POINT = new Color(51, 102, 204); |
| | | private static final Color HANDHELD_BOUNDARY_LABEL = new Color(22, 62, 138); |
| | | private static final double BOUNDARY_CONTAINS_TOLERANCE = 0.05; |
| | | |
| | | // 组件引用 |
| | | private JPanel visualizationPanel; |
| | | private List<Point2D.Double> currentBoundary; |
| | | private Rectangle2D.Double boundaryBounds; |
| | | private Path2D.Double currentBoundaryPath; |
| | | private List<Point2D.Double> currentPlannedPath; |
| | | private Rectangle2D.Double plannedPathBounds; |
| | | private List<Obstacledge.Obstacle> currentObstacles; |
| | |
| | | private long lastTrackPersistTimeMillis; |
| | | private boolean trackDirty; |
| | | private boolean handheldBoundaryPreviewActive; |
| | | private boolean pendingTrackBreak = true; |
| | | |
| | | private static final double TRACK_SAMPLE_MIN_DISTANCE_METERS = 0.1d; |
| | | private static final long TRACK_PERSIST_INTERVAL_MS = 5_000L; |
| | |
| | | } |
| | | |
| | | private void captureRealtimeTrackPoint() { |
| | | if (!realtimeTrackRecording) { |
| | | return; |
| | | } |
| | | if (realtimeTrackLandNumber == null || visualizationPanel == null) { |
| | | pendingTrackBreak = true; |
| | | return; |
| | | } |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | pendingTrackBreak = true; |
| | | return; |
| | | } |
| | | |
| | | String fixQuality = device.getPositioningStatus(); |
| | | if (!isHighPrecisionFix(fixQuality)) { |
| | | pendingTrackBreak = true; |
| | | return; |
| | | } |
| | | Point2D.Double position = mower.getPosition(); |
| | | if (position == null || !Double.isFinite(position.x) || !Double.isFinite(position.y)) { |
| | | pendingTrackBreak = true; |
| | | return; |
| | | } |
| | | |
| | | if (!isPointInsideActiveBoundary(position)) { |
| | | pendingTrackBreak = true; |
| | | return; |
| | | } |
| | | |
| | | Point2D.Double lastPoint = realtimeMowingTrack.isEmpty() ? null : realtimeMowingTrack.get(realtimeMowingTrack.size() - 1); |
| | | if (pendingTrackBreak) { |
| | | lastPoint = null; |
| | | } |
| | | double distance = 0.0; |
| | | if (lastPoint != null) { |
| | | double dx = position.x - lastPoint.x; |
| | |
| | | updateCompletionMetrics(); |
| | | trackDirty = true; |
| | | maybePersistRealtimeTrack(false); |
| | | pendingTrackBreak = false; |
| | | } |
| | | |
| | | private void updateCompletionMetrics() { |
| | |
| | | |
| | | realtimeTrackLandNumber = normalizedLand; |
| | | realtimeTrackRecording = true; |
| | | pendingTrackBreak = true; |
| | | captureRealtimeTrackPoint(); |
| | | } |
| | | |
| | | public void pauseRealtimeTrackRecording() { |
| | | realtimeTrackRecording = false; |
| | | pendingTrackBreak = true; |
| | | maybePersistRealtimeTrack(true); |
| | | } |
| | | |
| | | public void stopRealtimeTrackRecording() { |
| | | realtimeTrackRecording = false; |
| | | pendingTrackBreak = true; |
| | | maybePersistRealtimeTrack(true); |
| | | } |
| | | |
| | |
| | | completedMowingAreaSqMeters = 0.0; |
| | | mowingCompletionRatio = 0.0; |
| | | trackDirty = true; |
| | | pendingTrackBreak = true; |
| | | maybePersistRealtimeTrack(true); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public double getMowingCompletionRatio() { |
| | | if (!isMowerInsideSelectedBoundary()) { |
| | | return 0.0; |
| | | } |
| | | return mowingCompletionRatio; |
| | | } |
| | | |
| | | public double getCompletedMowingAreaSqMeters() { |
| | | if (!isMowerInsideSelectedBoundary()) { |
| | | return 0.0; |
| | | } |
| | | return completedMowingAreaSqMeters; |
| | | } |
| | | |
| | |
| | | return trackLengthMeters; |
| | | } |
| | | |
| | | private boolean isMowerInsideSelectedBoundary() { |
| | | Point2D.Double position = mower.getPosition(); |
| | | if (position == null) { |
| | | return false; |
| | | } |
| | | return isPointInsideActiveBoundary(position); |
| | | } |
| | | |
| | | public void flushRealtimeTrack() { |
| | | maybePersistRealtimeTrack(true); |
| | | } |
| | |
| | | mowingCompletionRatio = 0.0; |
| | | trackDirty = false; |
| | | lastTrackPersistTimeMillis = 0L; |
| | | pendingTrackBreak = true; |
| | | |
| | | String trimmed = normalizeValue(trackData); |
| | | if (trimmed == null || trimmed.isEmpty()) { |
| | |
| | | mowerNumberValueLabel.setText(formatDeviceValue(device.getMowerNumber())); |
| | | realtimeXValueLabel.setText(formatDeviceValue(device.getRealtimeX())); |
| | | realtimeYValueLabel.setText(formatDeviceValue(device.getRealtimeY())); |
| | | positioningStatusValueLabel.setText(formatDeviceValue(device.getPositioningStatus())); |
| | | positioningStatusValueLabel.setText(formatFixQualityValue(device.getPositioningStatus())); |
| | | satelliteCountValueLabel.setText(formatDeviceValue(device.getSatelliteCount())); |
| | | realtimeSpeedValueLabel.setText(formatDeviceValue(device.getRealtimeSpeed())); |
| | | headingValueLabel.setText(formatDeviceValue(device.getHeading())); |
| | |
| | | return sanitized == null ? "--" : sanitized; |
| | | } |
| | | |
| | | private String formatFixQualityValue(String value) { |
| | | String sanitized = sanitizeDeviceValue(value); |
| | | if (sanitized == null) { |
| | | return "--"; |
| | | } |
| | | switch (sanitized) { |
| | | case "0": |
| | | return "未定位"; |
| | | case "1": |
| | | return "单点定位"; |
| | | case "2": |
| | | return "码差分"; |
| | | case "3": |
| | | return "无效PPS"; |
| | | case "4": |
| | | return "固定解"; |
| | | case "5": |
| | | return "浮点解"; |
| | | case "6": |
| | | return "正在估算"; |
| | | case "7": |
| | | return "人工输入固定值"; |
| | | case "8": |
| | | return "模拟模式"; |
| | | case "9": |
| | | return "WAAS差分"; |
| | | default: |
| | | return sanitized; |
| | | } |
| | | } |
| | | |
| | | private String formatTimestamp(String value) { |
| | | String sanitized = sanitizeDeviceValue(value); |
| | | if (sanitized == null) { |
| | |
| | | |
| | | if (updated.size() < 2) { |
| | | currentBoundary = null; |
| | | currentBoundaryPath = null; |
| | | boundaryBounds = null; |
| | | boundaryPointsVisible = false; |
| | | Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, false); |
| | |
| | | adjustViewAfterBoundaryReset(); |
| | | } else { |
| | | currentBoundary = updated; |
| | | rebuildBoundaryPath(); |
| | | boundaryBounds = computeBounds(updated); |
| | | Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, boundaryPointsVisible); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | pendingTrackBreak = true; |
| | | } |
| | | |
| | | private boolean persistBoundaryChanges(List<Point2D.Double> updatedBoundary) { |
| | |
| | | return Math.hypot(dx, dy) <= BOUNDARY_POINT_MERGE_THRESHOLD; |
| | | } |
| | | |
| | | private boolean isHighPrecisionFix(String fixQuality) { |
| | | if (fixQuality == null) { |
| | | return false; |
| | | } |
| | | String trimmed = fixQuality.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | return false; |
| | | } |
| | | if ("4".equals(trimmed)) { |
| | | return true; |
| | | } |
| | | try { |
| | | double value = Double.parseDouble(trimmed); |
| | | return Math.abs(value - 4.0d) < 1e-6; |
| | | } catch (NumberFormatException ex) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private boolean isPointInsideActiveBoundary(Point2D.Double point) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | | return false; |
| | | } |
| | | if (realtimeTrackLandNumber == null) { |
| | | return false; |
| | | } |
| | | if (currentBoundaryLandNumber != null && !currentBoundaryLandNumber.equals(realtimeTrackLandNumber)) { |
| | | return false; |
| | | } |
| | | |
| | | Path2D.Double path = currentBoundaryPath; |
| | | if (path == null) { |
| | | path = buildBoundaryPath(currentBoundary); |
| | | currentBoundaryPath = path; |
| | | } |
| | | if (path == null) { |
| | | return false; |
| | | } |
| | | if (path.contains(point.x, point.y)) { |
| | | return true; |
| | | } |
| | | double size = BOUNDARY_CONTAINS_TOLERANCE * 2.0; |
| | | return path.intersects(point.x - BOUNDARY_CONTAINS_TOLERANCE, point.y - BOUNDARY_CONTAINS_TOLERANCE, size, size); |
| | | } |
| | | |
| | | private void rebuildBoundaryPath() { |
| | | currentBoundaryPath = buildBoundaryPath(currentBoundary); |
| | | } |
| | | |
| | | private Path2D.Double buildBoundaryPath(List<Point2D.Double> boundary) { |
| | | if (boundary == null || boundary.size() < 3) { |
| | | return null; |
| | | } |
| | | Path2D.Double path = new Path2D.Double(); |
| | | boolean started = false; |
| | | for (Point2D.Double point : boundary) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | | continue; |
| | | } |
| | | if (!started) { |
| | | path.moveTo(point.x, point.y); |
| | | started = true; |
| | | } else { |
| | | path.lineTo(point.x, point.y); |
| | | } |
| | | } |
| | | if (!started) { |
| | | return null; |
| | | } |
| | | path.closePath(); |
| | | return path; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 绘制视图信息 |
| | |
| | | } |
| | | |
| | | currentBoundary = parsed; |
| | | rebuildBoundaryPath(); |
| | | pendingTrackBreak = true; |
| | | boundaryBounds = computeBounds(parsed); |
| | | |
| | | Rectangle2D.Double bounds = boundaryBounds; |
| | |
| | | |
| | | private void clearBoundaryData() { |
| | | currentBoundary = null; |
| | | currentBoundaryPath = null; |
| | | boundaryBounds = null; |
| | | boundaryName = null; |
| | | boundaryPointsVisible = false; |
| | | currentBoundaryLandNumber = null; |
| | | pendingTrackBreak = true; |
| | | } |
| | | |
| | | public void setCurrentObstacles(String obstaclesData, String landNumber) { |