| | |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | | import java.awt.FontMetrics; |
| | | import java.awt.geom.AffineTransform; |
| | | import java.awt.geom.Ellipse2D; |
| | | import java.awt.geom.Path2D; |
| | | import java.awt.geom.Point2D; |
| | | import java.awt.geom.Rectangle2D; |
| | | import java.math.BigDecimal; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Set; |
| | | import gecaoji.Device; |
| | | import gecaoji.Gecaoji; |
| | | import dikuai.Dikuaiguanli; |
| | | import dikuai.Dikuai; |
| | | import zhangaiwu.Obstacledraw; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhangaiwu.yulanzhangaiwu; |
| | | |
| | | /** |
| | | * 地图渲染器 - 负责坐标系绘制、视图变换等功能 |
| | |
| | | private static final Color GRASS_BORDER_COLOR = new Color(60, 179, 113); |
| | | private static final Color BOUNDARY_POINT_COLOR = new Color(128, 0, 128); |
| | | private static final Color BOUNDARY_LABEL_COLOR = Color.BLACK; |
| | | private static final Color CIRCLE_SAMPLE_COLOR = new Color(220, 20, 60, 230); |
| | | private static final double CIRCLE_SAMPLE_SIZE = 0.54d; |
| | | private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05; |
| | | private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | private static final Color HANDHELD_BOUNDARY_FILL = new Color(51, 153, 255, 60); |
| | | 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 Rectangle2D.Double obstacleBounds; |
| | | private String selectedObstacleName; |
| | | private String currentObstacleLandNumber; |
| | | private String boundaryName; |
| | | private boolean boundaryPointsVisible; |
| | | private String currentBoundaryLandNumber; |
| | |
| | | private JLabel realtimeSpeedValueLabel; |
| | | private JLabel headingValueLabel; |
| | | private JLabel updateTimeValueLabel; |
| | | private CircleCaptureOverlay circleCaptureOverlay; |
| | | private final List<double[]> circleSampleMarkers = new ArrayList<>(); |
| | | private final List<Point2D.Double> realtimeMowingTrack = new ArrayList<>(); |
| | | private final List<Point2D.Double> handheldBoundaryPreview = new ArrayList<>(); |
| | | private boolean realtimeTrackRecording; |
| | | private String realtimeTrackLandNumber; |
| | | private double mowerEffectiveWidthMeters; |
| | | private double defaultMowerWidthMeters; |
| | | private double totalLandAreaSqMeters; |
| | | private double trackLengthMeters; |
| | | private double completedMowingAreaSqMeters; |
| | | private double mowingCompletionRatio; |
| | | 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; |
| | | |
| | | public MapRenderer(JPanel visualizationPanel) { |
| | | this.visualizationPanel = visualizationPanel; |
| | |
| | | private Timer createMowerTimer() { |
| | | Timer timer = new Timer(300, e -> { |
| | | mower.refreshFromDevice(); |
| | | if (realtimeTrackRecording) { |
| | | captureRealtimeTrackPoint(); |
| | | } |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | |
| | | |
| | | boolean hasBoundary = currentBoundary != null && currentBoundary.size() >= 2; |
| | | boolean hasPlannedPath = currentPlannedPath != null && currentPlannedPath.size() >= 2; |
| | | boolean hasObstacles = currentObstacles != null && !currentObstacles.isEmpty(); |
| | | |
| | | if (hasBoundary) { |
| | | drawCurrentBoundary(g2d); |
| | | } |
| | | |
| | | if (hasObstacles) { |
| | | Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName); |
| | | } |
| | | |
| | | yulanzhangaiwu.renderPreview(g2d, scale); |
| | | |
| | | if (!circleSampleMarkers.isEmpty()) { |
| | | drawCircleSampleMarkers(g2d, circleSampleMarkers, scale); |
| | | } |
| | | |
| | | if (circleCaptureOverlay != null) { |
| | | drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale); |
| | | } |
| | | |
| | | if (handheldBoundaryPreviewActive) { |
| | | drawHandheldBoundaryPreview(g2d); |
| | | } |
| | | |
| | | if (hasPlannedPath) { |
| | | drawCurrentPlannedPath(g2d); |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | if (!realtimeMowingTrack.isEmpty()) { |
| | | drawRealtimeMowingCoverage(g2d); |
| | | } |
| | | |
| | | drawMower(g2d); |
| | | |
| | | // 恢复原始变换 |
| | |
| | | mower.draw(g2d, scale); |
| | | } |
| | | |
| | | private void drawRealtimeMowingCoverage(Graphics2D g2d) { |
| | | if (realtimeMowingTrack.size() < 2) { |
| | | return; |
| | | } |
| | | |
| | | Path2D.Double path = new Path2D.Double(); |
| | | boolean started = false; |
| | | for (Point2D.Double point : realtimeMowingTrack) { |
| | | 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; |
| | | } |
| | | |
| | | Stroke originalStroke = g2d.getStroke(); |
| | | Color originalColor = g2d.getColor(); |
| | | |
| | | double effectiveWidth = getEffectiveMowerWidthMeters(); |
| | | if (effectiveWidth > 0) { |
| | | float coverageWidth = (float) Math.max(effectiveWidth, 0.1d); |
| | | g2d.setStroke(new BasicStroke(coverageWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(new Color(46, 139, 87, 80)); |
| | | g2d.draw(path); |
| | | } |
| | | |
| | | float spineWidth = (float) (effectiveWidth > 0 ? Math.max(effectiveWidth / 4.0, 0.18d) : 0.3d); |
| | | g2d.setStroke(new BasicStroke(spineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(new Color(34, 139, 34, 200)); |
| | | g2d.draw(path); |
| | | |
| | | g2d.setStroke(originalStroke); |
| | | g2d.setColor(originalColor); |
| | | } |
| | | |
| | | private void drawHandheldBoundaryPreview(Graphics2D g2d) { |
| | | if (!handheldBoundaryPreviewActive || handheldBoundaryPreview.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | Path2D.Double path = new Path2D.Double(); |
| | | boolean started = false; |
| | | for (Point2D.Double point : handheldBoundaryPreview) { |
| | | 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; |
| | | } |
| | | |
| | | Stroke originalStroke = g2d.getStroke(); |
| | | Color originalColor = g2d.getColor(); |
| | | |
| | | if (handheldBoundaryPreview.size() >= 3) { |
| | | path.closePath(); |
| | | g2d.setColor(HANDHELD_BOUNDARY_FILL); |
| | | g2d.fill(path); |
| | | } |
| | | |
| | | float outlineWidth = (float) Math.max(1.5f / scale, 0.2f); |
| | | g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | g2d.draw(path); |
| | | |
| | | double markerSize = Math.max(0.5d, 3.0d / scale); |
| | | double markerRadius = markerSize / 2.0d; |
| | | Font originalFont = g2d.getFont(); |
| | | float labelFontSize = (float) Math.max(6.0f, 16.0f / Math.max(scale, 0.2)); |
| | | Font labelFont = originalFont.deriveFont(Font.BOLD, labelFontSize); |
| | | FontMetrics metrics = g2d.getFontMetrics(labelFont); |
| | | |
| | | for (Point2D.Double point : handheldBoundaryPreview) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | | continue; |
| | | } |
| | | Shape marker = new Ellipse2D.Double(point.x - markerRadius, point.y - markerRadius, markerSize, markerSize); |
| | | g2d.setColor(HANDHELD_BOUNDARY_POINT); |
| | | g2d.fill(marker); |
| | | } |
| | | |
| | | g2d.setFont(labelFont); |
| | | g2d.setColor(HANDHELD_BOUNDARY_LABEL); |
| | | int index = 1; |
| | | for (Point2D.Double point : handheldBoundaryPreview) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | | index++; |
| | | continue; |
| | | } |
| | | String label = String.valueOf(index++); |
| | | int textWidth = metrics.stringWidth(label); |
| | | int ascent = metrics.getAscent(); |
| | | int descent = metrics.getDescent(); |
| | | float textX = (float) (point.x - textWidth / 2.0); |
| | | float textY = (float) (point.y + (ascent - descent) / 2.0); |
| | | g2d.drawString(label, textX, textY); |
| | | } |
| | | |
| | | g2d.setStroke(originalStroke); |
| | | g2d.setFont(originalFont); |
| | | g2d.setColor(originalColor); |
| | | } |
| | | |
| | | 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; |
| | | double dy = position.y - lastPoint.y; |
| | | distance = Math.hypot(dx, dy); |
| | | if (distance < TRACK_SAMPLE_MIN_DISTANCE_METERS) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | realtimeMowingTrack.add(new Point2D.Double(position.x, position.y)); |
| | | if (distance > 0.0) { |
| | | trackLengthMeters += distance; |
| | | } |
| | | |
| | | updateCompletionMetrics(); |
| | | trackDirty = true; |
| | | maybePersistRealtimeTrack(false); |
| | | pendingTrackBreak = false; |
| | | } |
| | | |
| | | private void updateCompletionMetrics() { |
| | | double widthMeters = getEffectiveMowerWidthMeters(); |
| | | if (widthMeters > 0 && trackLengthMeters > 0) { |
| | | completedMowingAreaSqMeters = trackLengthMeters * widthMeters; |
| | | } else { |
| | | completedMowingAreaSqMeters = 0.0; |
| | | } |
| | | |
| | | if (totalLandAreaSqMeters > 0 && completedMowingAreaSqMeters >= 0) { |
| | | mowingCompletionRatio = Math.max(0.0, Math.min(1.0, completedMowingAreaSqMeters / totalLandAreaSqMeters)); |
| | | } else { |
| | | mowingCompletionRatio = 0.0; |
| | | } |
| | | } |
| | | |
| | | private void maybePersistRealtimeTrack(boolean force) { |
| | | if (!trackDirty) { |
| | | return; |
| | | } |
| | | long now = System.currentTimeMillis(); |
| | | if (!force && (now - lastTrackPersistTimeMillis) < TRACK_PERSIST_INTERVAL_MS) { |
| | | return; |
| | | } |
| | | persistRealtimeTrack(); |
| | | } |
| | | |
| | | private void persistRealtimeTrack() { |
| | | if (realtimeTrackLandNumber == null) { |
| | | trackDirty = false; |
| | | return; |
| | | } |
| | | String serialized = serializeRealtimeTrack(); |
| | | String storedValue = (serialized == null || serialized.isEmpty()) ? "-1" : serialized; |
| | | boolean updated = Dikuai.updateField(realtimeTrackLandNumber, "mowingTrack", storedValue); |
| | | if (updated) { |
| | | Dikuai dikuai = Dikuai.getDikuai(realtimeTrackLandNumber); |
| | | if (dikuai != null) { |
| | | dikuai.setMowingTrack(storedValue); |
| | | } |
| | | Dikuai.saveToProperties(); |
| | | trackDirty = false; |
| | | lastTrackPersistTimeMillis = System.currentTimeMillis(); |
| | | } |
| | | } |
| | | |
| | | private String serializeRealtimeTrack() { |
| | | if (realtimeMowingTrack.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (Point2D.Double point : realtimeMowingTrack) { |
| | | if (point == null) { |
| | | continue; |
| | | } |
| | | if (builder.length() > 0) { |
| | | builder.append(';'); |
| | | } |
| | | builder.append(formatTrackCoordinate(point.x)).append(',').append(formatTrackCoordinate(point.y)); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String formatTrackCoordinate(double value) { |
| | | if (!Double.isFinite(value)) { |
| | | return "0"; |
| | | } |
| | | return String.format(Locale.US, "%.3f", value); |
| | | } |
| | | |
| | | private double getEffectiveMowerWidthMeters() { |
| | | if (mowerEffectiveWidthMeters > 0) { |
| | | return mowerEffectiveWidthMeters; |
| | | } |
| | | if (defaultMowerWidthMeters > 0) { |
| | | return defaultMowerWidthMeters; |
| | | } |
| | | return 0.0; |
| | | } |
| | | |
| | | public void applyLandMetadata(Dikuai dikuai) { |
| | | String landNumber = normalizeValue(dikuai != null ? dikuai.getLandNumber() : null); |
| | | totalLandAreaSqMeters = parseLandAreaSqMeters(dikuai != null ? dikuai.getLandArea() : null); |
| | | defaultMowerWidthMeters = parseMowerWidthMeters(dikuai != null ? dikuai.getMowingWidth() : null); |
| | | |
| | | // 若当前未录制或切换地块,则更新有效割草宽度 |
| | | if (!realtimeTrackRecording || !equalsLand(landNumber, realtimeTrackLandNumber)) { |
| | | mowerEffectiveWidthMeters = defaultMowerWidthMeters; |
| | | } |
| | | |
| | | loadRealtimeTrack(landNumber, dikuai != null ? dikuai.getMowingTrack() : null); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void startRealtimeTrackRecording(String landNumber, double widthMeters) { |
| | | String normalizedLand = normalizeValue(landNumber); |
| | | if (normalizedLand == null) { |
| | | return; |
| | | } |
| | | |
| | | if (!equalsLand(normalizedLand, realtimeTrackLandNumber)) { |
| | | Dikuai dikuai = Dikuai.getDikuai(normalizedLand); |
| | | totalLandAreaSqMeters = parseLandAreaSqMeters(dikuai != null ? dikuai.getLandArea() : null); |
| | | defaultMowerWidthMeters = parseMowerWidthMeters(dikuai != null ? dikuai.getMowingWidth() : null); |
| | | loadRealtimeTrack(normalizedLand, dikuai != null ? dikuai.getMowingTrack() : null); |
| | | } |
| | | |
| | | if (widthMeters > 0) { |
| | | mowerEffectiveWidthMeters = widthMeters; |
| | | } else if (mowerEffectiveWidthMeters <= 0) { |
| | | mowerEffectiveWidthMeters = defaultMowerWidthMeters; |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | public void forceRealtimeTrackSnapshot() { |
| | | if (!realtimeTrackRecording) { |
| | | return; |
| | | } |
| | | captureRealtimeTrackPoint(); |
| | | } |
| | | |
| | | public void clearRealtimeTrack() { |
| | | realtimeTrackRecording = false; |
| | | realtimeMowingTrack.clear(); |
| | | trackLengthMeters = 0.0; |
| | | 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; |
| | | } |
| | | |
| | | public double getTotalLandAreaSqMeters() { |
| | | return totalLandAreaSqMeters; |
| | | } |
| | | |
| | | public double getTrackLengthMeters() { |
| | | return trackLengthMeters; |
| | | } |
| | | |
| | | private boolean isMowerInsideSelectedBoundary() { |
| | | Point2D.Double position = mower.getPosition(); |
| | | if (position == null) { |
| | | return false; |
| | | } |
| | | return isPointInsideActiveBoundary(position); |
| | | } |
| | | |
| | | public void flushRealtimeTrack() { |
| | | maybePersistRealtimeTrack(true); |
| | | } |
| | | |
| | | private void loadRealtimeTrack(String landNumber, String trackData) { |
| | | realtimeTrackRecording = false; |
| | | realtimeTrackLandNumber = landNumber; |
| | | realtimeMowingTrack.clear(); |
| | | trackLengthMeters = 0.0; |
| | | completedMowingAreaSqMeters = 0.0; |
| | | mowingCompletionRatio = 0.0; |
| | | trackDirty = false; |
| | | lastTrackPersistTimeMillis = 0L; |
| | | pendingTrackBreak = true; |
| | | |
| | | String trimmed = normalizeValue(trackData); |
| | | if (trimmed == null || trimmed.isEmpty()) { |
| | | updateCompletionMetrics(); |
| | | return; |
| | | } |
| | | |
| | | String[] segments = trimmed.split(";"); |
| | | Point2D.Double lastPoint = null; |
| | | for (String segment : segments) { |
| | | if (segment == null || segment.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | String[] parts = segment.trim().split(","); |
| | | if (parts.length < 2) { |
| | | continue; |
| | | } |
| | | try { |
| | | double x = Double.parseDouble(parts[0].trim()); |
| | | double y = Double.parseDouble(parts[1].trim()); |
| | | Point2D.Double current = new Point2D.Double(x, y); |
| | | realtimeMowingTrack.add(current); |
| | | if (lastPoint != null) { |
| | | trackLengthMeters += Math.hypot(current.x - lastPoint.x, current.y - lastPoint.y); |
| | | } |
| | | lastPoint = current; |
| | | } catch (NumberFormatException ignored) { |
| | | // 跳过异常条目 |
| | | } |
| | | } |
| | | |
| | | updateCompletionMetrics(); |
| | | } |
| | | |
| | | private double parseLandAreaSqMeters(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double area = Double.parseDouble(trimmed); |
| | | return area > 0 ? area : 0.0; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private double parseMowerWidthMeters(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String sanitized = raw.trim().toLowerCase(Locale.ROOT); |
| | | if (sanitized.isEmpty() || "-1".equals(sanitized)) { |
| | | return 0.0; |
| | | } |
| | | sanitized = sanitized.replace("厘米", "cm"); |
| | | sanitized = sanitized.replace("公分", "cm"); |
| | | sanitized = sanitized.replace("米", "m"); |
| | | sanitized = sanitized.replace("cm", ""); |
| | | sanitized = sanitized.replace("m", ""); |
| | | sanitized = sanitized.trim(); |
| | | if (sanitized.isEmpty()) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double value = Double.parseDouble(sanitized); |
| | | if (value <= 0) { |
| | | return 0.0; |
| | | } |
| | | if (value > 10) { |
| | | return value / 100.0; // 视为厘米 |
| | | } |
| | | return value; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private String normalizeValue(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private boolean equalsLand(String a, String b) { |
| | | if (a == null && b == null) { |
| | | return true; |
| | | } |
| | | if (a == null || b == null) { |
| | | return false; |
| | | } |
| | | return a.equals(b); |
| | | } |
| | | |
| | | private boolean handleMowerClick(Point screenPoint) { |
| | | if (!mower.hasValidPosition()) { |
| | | return false; |
| | |
| | | lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale); |
| | | } |
| | | |
| | | private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) { |
| | | if (markers == null || markers.isEmpty()) { |
| | | return; |
| | | } |
| | | Shape markerShape; |
| | | double half = CIRCLE_SAMPLE_SIZE / 2.0; |
| | | g2d.setColor(CIRCLE_SAMPLE_COLOR); |
| | | g2d.setStroke(new BasicStroke((float) (1.2f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | Font originalFont = g2d.getFont(); |
| | | float baseSize = (float) Math.max(12f / scale, 9f); |
| | | float reducedSize = Math.max(baseSize / 3f, 3f); |
| | | Font labelFont = originalFont.deriveFont(reducedSize); |
| | | g2d.setFont(labelFont); |
| | | FontMetrics metrics = g2d.getFontMetrics(); |
| | | for (double[] pt : markers) { |
| | | if (pt == null || pt.length < 2 || !Double.isFinite(pt[0]) || !Double.isFinite(pt[1])) { |
| | | continue; |
| | | } |
| | | double x = pt[0]; |
| | | double y = pt[1]; |
| | | markerShape = new Ellipse2D.Double(x - half, y - half, CIRCLE_SAMPLE_SIZE, CIRCLE_SAMPLE_SIZE); |
| | | g2d.fill(markerShape); |
| | | String label = String.format(Locale.US, "%.2f,%.2f", x, y); |
| | | int textWidth = metrics.stringWidth(label); |
| | | float textX = (float) (x - textWidth / 2.0); |
| | | float textY = (float) (y - half - 0.2d) - metrics.getDescent(); |
| | | g2d.setColor(new Color(33, 37, 41, 220)); |
| | | g2d.drawString(label, textX, textY); |
| | | g2d.setColor(CIRCLE_SAMPLE_COLOR); |
| | | } |
| | | g2d.setFont(originalFont); |
| | | } |
| | | |
| | | private void drawCircleCaptureOverlay(Graphics2D g2d, CircleCaptureOverlay overlay, double scale) { |
| | | double diameter = overlay.radius * 2.0; |
| | | Ellipse2D outline = new Ellipse2D.Double( |
| | | overlay.centerX - overlay.radius, |
| | | overlay.centerY - overlay.radius, |
| | | diameter, |
| | | diameter); |
| | | |
| | | Color fillColor = new Color(255, 152, 0, 80); |
| | | Color borderColor = new Color(255, 87, 34, 230); |
| | | Color centerColor = new Color(46, 139, 87, 230); |
| | | |
| | | g2d.setColor(fillColor); |
| | | g2d.fill(outline); |
| | | |
| | | g2d.setColor(borderColor); |
| | | g2d.setStroke(new BasicStroke((float) (1.8f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.draw(outline); |
| | | |
| | | double markerSize = 0.18d; |
| | | double centerMarkerSize = 0.22d; |
| | | |
| | | Ellipse2D centerMarker = new Ellipse2D.Double( |
| | | overlay.centerX - centerMarkerSize / 2.0, |
| | | overlay.centerY - centerMarkerSize / 2.0, |
| | | centerMarkerSize, |
| | | centerMarkerSize); |
| | | g2d.setColor(centerColor); |
| | | g2d.fill(centerMarker); |
| | | } |
| | | |
| | | public void showCircleCaptureOverlay(double centerX, double centerY, double radius, List<double[]> samplePoints) { |
| | | List<double[]> copies = new ArrayList<>(); |
| | | if (samplePoints != null) { |
| | | for (double[] pt : samplePoints) { |
| | | if (pt == null || pt.length < 2) { |
| | | continue; |
| | | } |
| | | copies.add(new double[]{pt[0], pt[1]}); |
| | | } |
| | | } |
| | | circleCaptureOverlay = new CircleCaptureOverlay(centerX, centerY, radius, copies); |
| | | updateCircleSampleMarkers(samplePoints); |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | public void clearCircleCaptureOverlay() { |
| | | circleCaptureOverlay = null; |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | public void updateCircleSampleMarkers(List<double[]> samplePoints) { |
| | | circleSampleMarkers.clear(); |
| | | if (samplePoints != null) { |
| | | for (double[] pt : samplePoints) { |
| | | if (pt == null || pt.length < 2) { |
| | | continue; |
| | | } |
| | | double x = pt[0]; |
| | | double y = pt[1]; |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | continue; |
| | | } |
| | | circleSampleMarkers.add(new double[]{x, y}); |
| | | } |
| | | } |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | public void clearCircleSampleMarkers() { |
| | | if (!circleSampleMarkers.isEmpty()) { |
| | | circleSampleMarkers.clear(); |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static final class CircleCaptureOverlay { |
| | | final double centerX; |
| | | final double centerY; |
| | | final double radius; |
| | | final List<double[]> samplePoints; |
| | | |
| | | CircleCaptureOverlay(double centerX, double centerY, double radius, List<double[]> samplePoints) { |
| | | this.centerX = centerX; |
| | | this.centerY = centerY; |
| | | this.radius = radius; |
| | | this.samplePoints = samplePoints; |
| | | } |
| | | } |
| | | |
| | | private void showMowerInfo() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | |
| | | 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())); |
| | |
| | | if (mowerNumberValueLabel != null) mowerNumberValueLabel.setText(value); |
| | | if (realtimeXValueLabel != null) realtimeXValueLabel.setText(value); |
| | | if (realtimeYValueLabel != null) realtimeYValueLabel.setText(value); |
| | | if (positioningStatusValueLabel != null) positioningStatusValueLabel.setText(value); |
| | | if (positioningStatusValueLabel != null) positioningStatusValueLabel.setText(value); |
| | | if (satelliteCountValueLabel != null) satelliteCountValueLabel.setText(value); |
| | | if (realtimeSpeedValueLabel != null) realtimeSpeedValueLabel.setText(value); |
| | | if (headingValueLabel != null) headingValueLabel.setText(value); |
| | |
| | | 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; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 绘制视图信息 |
| | |
| | | return; |
| | | } |
| | | |
| | | currentBoundary = parsed; |
| | | boundaryBounds = computeBounds(parsed); |
| | | currentBoundary = parsed; |
| | | rebuildBoundaryPath(); |
| | | pendingTrackBreak = true; |
| | | boundaryBounds = computeBounds(parsed); |
| | | |
| | | Rectangle2D.Double bounds = boundaryBounds; |
| | | SwingUtilities.invokeLater(() -> { |
| | |
| | | |
| | | private void clearBoundaryData() { |
| | | currentBoundary = null; |
| | | currentBoundaryPath = null; |
| | | boundaryBounds = null; |
| | | boundaryName = null; |
| | | boundaryPointsVisible = false; |
| | | currentBoundaryLandNumber = null; |
| | | pendingTrackBreak = true; |
| | | } |
| | | |
| | | public void setCurrentObstacles(String obstaclesData, String landNumber) { |
| | | List<Obstacledge.Obstacle> parsed = parseObstacles(obstaclesData, landNumber); |
| | | applyObstaclesToRenderer(parsed, landNumber); |
| | | } |
| | | |
| | | public void setCurrentObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) { |
| | | List<Obstacledge.Obstacle> cloned = cloneObstacles(obstacles, landNumber); |
| | | applyObstaclesToRenderer(cloned, landNumber); |
| | | } |
| | | |
| | | private void applyObstaclesToRenderer(List<Obstacledge.Obstacle> obstacles, String landNumber) { |
| | | List<Obstacledge.Obstacle> safeList = obstacles != null ? obstacles : Collections.emptyList(); |
| | | String normalizedLand = (landNumber != null) ? landNumber.trim() : null; |
| | | |
| | | if (normalizedLand != null && !normalizedLand.isEmpty()) { |
| | | safeList = filterObstaclesForLand(safeList, normalizedLand); |
| | | } |
| | | |
| | | if (normalizedLand == null || normalizedLand.isEmpty()) { |
| | | clearObstacleData(); |
| | | if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) { |
| | | resetView(); |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (safeList.isEmpty()) { |
| | | clearObstacleData(); |
| | | if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) { |
| | | resetView(); |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | currentObstacles = Collections.unmodifiableList(new ArrayList<>(safeList)); |
| | | currentObstacleLandNumber = normalizedLand; |
| | | obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(currentObstacles)); |
| | | selectedObstacleName = null; |
| | | |
| | | if (!hasRenderableBoundary() && !hasRenderablePlannedPath() && obstacleBounds != null) { |
| | | Rectangle2D.Double bounds = obstacleBounds; |
| | | SwingUtilities.invokeLater(() -> { |
| | | fitBoundsToView(bounds); |
| | | visualizationPanel.repaint(); |
| | | }); |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | private List<Obstacledge.Obstacle> cloneObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) { |
| | | List<Obstacledge.Obstacle> result = new ArrayList<>(); |
| | | if (obstacles == null || obstacles.isEmpty()) { |
| | | return result; |
| | | } |
| | | |
| | | String normalizedLandNumber = landNumber != null ? landNumber.trim() : null; |
| | | Set<String> usedNames = new LinkedHashSet<>(); |
| | | int fallbackIndex = 1; |
| | | |
| | | for (Obstacledge.Obstacle source : obstacles) { |
| | | if (source == null) { |
| | | continue; |
| | | } |
| | | |
| | | if (normalizedLandNumber != null && !normalizedLandNumber.isEmpty()) { |
| | | String sourcePlotId = source.getPlotId(); |
| | | if (sourcePlotId != null && !sourcePlotId.trim().isEmpty() |
| | | && !normalizedLandNumber.equalsIgnoreCase(sourcePlotId.trim())) { |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | Obstacledge.ObstacleShape shape = source.getShape(); |
| | | List<Obstacledge.XYCoordinate> xySource = source.getXyCoordinates(); |
| | | if (shape == null || xySource == null) { |
| | | continue; |
| | | } |
| | | |
| | | List<Obstacledge.XYCoordinate> xyCopy = copyXYCoordinates(xySource); |
| | | if (shape == Obstacledge.ObstacleShape.CIRCLE && xyCopy.size() < 2) { |
| | | continue; |
| | | } |
| | | if (shape == Obstacledge.ObstacleShape.POLYGON && xyCopy.size() < 3) { |
| | | continue; |
| | | } |
| | | |
| | | String desiredName = source.getObstacleName(); |
| | | if (desiredName == null || desiredName.trim().isEmpty()) { |
| | | desiredName = "障碍物" + fallbackIndex++; |
| | | } |
| | | String uniqueName = ensureUniqueName(usedNames, desiredName.trim()); |
| | | |
| | | Obstacledge.Obstacle copy = new Obstacledge.Obstacle( |
| | | normalizedLandNumber != null ? normalizedLandNumber : source.getPlotId(), |
| | | uniqueName, |
| | | shape |
| | | ); |
| | | |
| | | copy.setXyCoordinates(xyCopy); |
| | | |
| | | List<Obstacledge.DMCoordinate> dmCopy = copyDMCoordinates(source.getOriginalCoordinates()); |
| | | if (dmCopy.isEmpty()) { |
| | | populateDummyOriginalCoordinates(copy, xyCopy.size()); |
| | | } else { |
| | | copy.setOriginalCoordinates(dmCopy); |
| | | } |
| | | |
| | | result.add(copy); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | private List<Obstacledge.Obstacle> filterObstaclesForLand(List<Obstacledge.Obstacle> obstacles, String landNumber) { |
| | | if (obstacles == null || obstacles.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | if (landNumber == null || landNumber.trim().isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | String normalized = landNumber.trim(); |
| | | List<Obstacledge.Obstacle> filtered = new ArrayList<>(); |
| | | for (Obstacledge.Obstacle obstacle : obstacles) { |
| | | if (obstacle == null) { |
| | | continue; |
| | | } |
| | | String plotId = obstacle.getPlotId(); |
| | | if (plotId == null || plotId.trim().isEmpty()) { |
| | | filtered.add(obstacle); |
| | | continue; |
| | | } |
| | | if (normalized.equalsIgnoreCase(plotId.trim())) { |
| | | filtered.add(obstacle); |
| | | } |
| | | } |
| | | return filtered; |
| | | } |
| | | |
| | | private String ensureUniqueName(Set<String> usedNames, String preferredName) { |
| | | String base = (preferredName == null || preferredName.trim().isEmpty()) ? "障碍物" : preferredName.trim(); |
| | | String normalized = base.toLowerCase(Locale.ROOT); |
| | | if (usedNames.add(normalized)) { |
| | | return base; |
| | | } |
| | | int suffix = 2; |
| | | while (true) { |
| | | String attempt = base + suffix; |
| | | String attemptKey = attempt.toLowerCase(Locale.ROOT); |
| | | if (usedNames.add(attemptKey)) { |
| | | return attempt; |
| | | } |
| | | suffix++; |
| | | } |
| | | } |
| | | |
| | | private List<Obstacledge.XYCoordinate> copyXYCoordinates(List<Obstacledge.XYCoordinate> source) { |
| | | List<Obstacledge.XYCoordinate> copy = new ArrayList<>(); |
| | | if (source == null) { |
| | | return copy; |
| | | } |
| | | for (Obstacledge.XYCoordinate coord : source) { |
| | | if (coord == null) { |
| | | continue; |
| | | } |
| | | double x = coord.getX(); |
| | | double y = coord.getY(); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | continue; |
| | | } |
| | | copy.add(new Obstacledge.XYCoordinate(x, y)); |
| | | } |
| | | return copy; |
| | | } |
| | | |
| | | private List<Obstacledge.DMCoordinate> copyDMCoordinates(List<Obstacledge.DMCoordinate> source) { |
| | | List<Obstacledge.DMCoordinate> copy = new ArrayList<>(); |
| | | if (source == null) { |
| | | return copy; |
| | | } |
| | | for (Obstacledge.DMCoordinate coord : source) { |
| | | if (coord == null) { |
| | | continue; |
| | | } |
| | | copy.add(new Obstacledge.DMCoordinate(coord.getDegreeMinute(), coord.getDirection())); |
| | | } |
| | | return copy; |
| | | } |
| | | |
| | | private void clearObstacleData() { |
| | | currentObstacles = null; |
| | | obstacleBounds = null; |
| | | selectedObstacleName = null; |
| | | currentObstacleLandNumber = null; |
| | | } |
| | | |
| | | private List<Obstacledge.Obstacle> parseObstacles(String obstaclesData, String landNumber) { |
| | | List<Obstacledge.Obstacle> obstacles = new ArrayList<>(); |
| | | if (obstaclesData == null) { |
| | | return obstacles; |
| | | } |
| | | |
| | | String normalized = stripInlineComment(obstaclesData.trim()); |
| | | if (normalized.isEmpty() || "-1".equals(normalized)) { |
| | | return obstacles; |
| | | } |
| | | |
| | | List<String> entries = splitObstacleEntries(normalized); |
| | | int defaultIndex = 1; |
| | | |
| | | for (String entry : entries) { |
| | | String trimmedEntry = stripInlineComment(entry); |
| | | if (trimmedEntry.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | String nameToken = null; |
| | | String shapeToken = null; |
| | | String coordsSection = trimmedEntry; |
| | | |
| | | if (trimmedEntry.contains("::")) { |
| | | String[] parts = trimmedEntry.split("::", 3); |
| | | if (parts.length == 3) { |
| | | nameToken = parts[0].trim(); |
| | | shapeToken = parts[1].trim(); |
| | | coordsSection = parts[2].trim(); |
| | | } |
| | | } else if (trimmedEntry.contains("@")) { |
| | | String[] parts = trimmedEntry.split("@", 3); |
| | | if (parts.length == 3) { |
| | | nameToken = parts[0].trim(); |
| | | shapeToken = parts[1].trim(); |
| | | coordsSection = parts[2].trim(); |
| | | } else if (parts.length == 2) { |
| | | shapeToken = parts[0].trim(); |
| | | coordsSection = parts[1].trim(); |
| | | } |
| | | } else if (trimmedEntry.contains(":")) { |
| | | String[] parts = trimmedEntry.split(":", 3); |
| | | if (parts.length == 3) { |
| | | nameToken = parts[0].trim(); |
| | | shapeToken = parts[1].trim(); |
| | | coordsSection = parts[2].trim(); |
| | | } else if (parts.length == 2) { |
| | | if (looksLikeShapeToken(parts[0])) { |
| | | shapeToken = parts[0].trim(); |
| | | coordsSection = parts[1].trim(); |
| | | } else { |
| | | nameToken = parts[0].trim(); |
| | | coordsSection = parts[1].trim(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | List<Obstacledge.XYCoordinate> xyCoordinates = parseObstacleCoordinates(coordsSection); |
| | | if (xyCoordinates.size() < 2) { |
| | | continue; |
| | | } |
| | | |
| | | Obstacledge.ObstacleShape shape = resolveObstacleShape(shapeToken, xyCoordinates.size()); |
| | | if (shape == null) { |
| | | continue; |
| | | } |
| | | |
| | | String obstacleName = (nameToken != null && !nameToken.isEmpty()) |
| | | ? nameToken |
| | | : "障碍物" + defaultIndex++; |
| | | |
| | | Obstacledge.Obstacle obstacle = new Obstacledge.Obstacle(landNumber, obstacleName, shape); |
| | | obstacle.setXyCoordinates(new ArrayList<>(xyCoordinates)); |
| | | populateDummyOriginalCoordinates(obstacle, xyCoordinates.size()); |
| | | |
| | | if (obstacle.isValid()) { |
| | | obstacles.add(obstacle); |
| | | } |
| | | } |
| | | |
| | | return obstacles; |
| | | } |
| | | |
| | | private boolean looksLikeShapeToken(String token) { |
| | | if (token == null) { |
| | | return false; |
| | | } |
| | | String normalized = token.trim().toLowerCase(Locale.ROOT); |
| | | return "circle".equals(normalized) |
| | | || "polygon".equals(normalized) |
| | | || "圆形".equals(normalized) |
| | | || "多边形".equals(normalized) |
| | | || "0".equals(normalized) |
| | | || "1".equals(normalized); |
| | | } |
| | | |
| | | private List<Obstacledge.XYCoordinate> parseObstacleCoordinates(String coordsSection) { |
| | | List<Obstacledge.XYCoordinate> coords = new ArrayList<>(); |
| | | if (coordsSection == null) { |
| | | return coords; |
| | | } |
| | | |
| | | String sanitized = stripInlineComment(coordsSection.trim()); |
| | | if (sanitized.isEmpty() || "-1".equals(sanitized)) { |
| | | return coords; |
| | | } |
| | | |
| | | String[] pairs = sanitized.split(";"); |
| | | for (String pair : pairs) { |
| | | if (pair == null) { |
| | | continue; |
| | | } |
| | | String trimmed = stripInlineComment(pair.trim()); |
| | | if (trimmed.isEmpty()) { |
| | | continue; |
| | | } |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length < 2) { |
| | | continue; |
| | | } |
| | | try { |
| | | double x = Double.parseDouble(parts[0].trim()); |
| | | double y = Double.parseDouble(parts[1].trim()); |
| | | coords.add(new Obstacledge.XYCoordinate(x, y)); |
| | | } catch (NumberFormatException ignored) { |
| | | // Skip malformed coordinate pair |
| | | } |
| | | } |
| | | |
| | | return coords; |
| | | } |
| | | |
| | | private Obstacledge.ObstacleShape resolveObstacleShape(String shapeToken, int coordinateCount) { |
| | | if (shapeToken != null && !shapeToken.trim().isEmpty()) { |
| | | String normalized = shapeToken.trim().toLowerCase(Locale.ROOT); |
| | | if ("circle".equals(normalized) || "圆形".equals(normalized) || "0".equals(normalized)) { |
| | | return Obstacledge.ObstacleShape.CIRCLE; |
| | | } |
| | | if ("polygon".equals(normalized) || "多边形".equals(normalized) || "1".equals(normalized)) { |
| | | return Obstacledge.ObstacleShape.POLYGON; |
| | | } |
| | | } |
| | | |
| | | if (coordinateCount == 2) { |
| | | return Obstacledge.ObstacleShape.CIRCLE; |
| | | } |
| | | if (coordinateCount >= 3) { |
| | | return Obstacledge.ObstacleShape.POLYGON; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private void populateDummyOriginalCoordinates(Obstacledge.Obstacle obstacle, int xyCount) { |
| | | List<Obstacledge.DMCoordinate> dmCoordinates = new ArrayList<>(); |
| | | int points = Math.max(1, xyCount); |
| | | for (int i = 0; i < points; i++) { |
| | | dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'N')); |
| | | dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'E')); |
| | | } |
| | | obstacle.setOriginalCoordinates(dmCoordinates); |
| | | } |
| | | |
| | | private List<String> splitObstacleEntries(String data) { |
| | | List<String> entries = new ArrayList<>(); |
| | | if (data.indexOf('|') >= 0) { |
| | | String[] parts = data.split("\\|"); |
| | | for (String part : parts) { |
| | | if (part != null && !part.trim().isEmpty()) { |
| | | entries.add(part.trim()); |
| | | } |
| | | } |
| | | } else if (data.contains("\n")) { |
| | | String[] lines = data.split("\r?\n"); |
| | | for (String line : lines) { |
| | | if (line != null && !line.trim().isEmpty()) { |
| | | entries.add(line.trim()); |
| | | } |
| | | } |
| | | } else { |
| | | entries.add(data); |
| | | } |
| | | return entries; |
| | | } |
| | | |
| | | private String stripInlineComment(String text) { |
| | | if (text == null) { |
| | | return ""; |
| | | } |
| | | int hashIndex = text.indexOf('#'); |
| | | if (hashIndex >= 0) { |
| | | return text.substring(0, hashIndex).trim(); |
| | | } |
| | | return text.trim(); |
| | | } |
| | | |
| | | private Rectangle2D.Double convertObstacleBounds(double[] bounds) { |
| | | if (bounds == null || bounds.length < 4) { |
| | | return null; |
| | | } |
| | | double minX = bounds[0]; |
| | | double minY = bounds[1]; |
| | | double maxX = bounds[2]; |
| | | double maxY = bounds[3]; |
| | | return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); |
| | | } |
| | | |
| | | private boolean hasRenderableBoundary() { |
| | | return currentBoundary != null && currentBoundary.size() >= 2; |
| | | } |
| | | |
| | | private boolean hasRenderablePlannedPath() { |
| | | return currentPlannedPath != null && currentPlannedPath.size() >= 2; |
| | | } |
| | | |
| | | private void adjustViewAfterBoundaryReset() { |
| | |
| | | fitBoundsToView(bounds); |
| | | visualizationPanel.repaint(); |
| | | }); |
| | | } else { |
| | | resetView(); |
| | | return; |
| | | } |
| | | |
| | | if (obstacleBounds != null) { |
| | | Rectangle2D.Double bounds = obstacleBounds; |
| | | SwingUtilities.invokeLater(() -> { |
| | | fitBoundsToView(bounds); |
| | | visualizationPanel.repaint(); |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | resetView(); |
| | | } |
| | | |
| | | public void setCurrentPlannedPath(String plannedPath) { |
| | | if (plannedPath == null) { |
| | | currentPlannedPath = null; |
| | | plannedPathBounds = null; |
| | | if (currentBoundary == null || currentBoundary.size() < 2) { |
| | | if (!hasRenderableBoundary()) { |
| | | resetView(); |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | |
| | | if (parsed.size() < 2) { |
| | | currentPlannedPath = null; |
| | | plannedPathBounds = null; |
| | | if (currentBoundary == null || currentBoundary.size() < 2) { |
| | | if (!hasRenderableBoundary()) { |
| | | resetView(); |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | |
| | | |
| | | Rectangle2D.Double bounds = plannedPathBounds; |
| | | SwingUtilities.invokeLater(() -> { |
| | | if (currentBoundary == null || currentBoundary.size() < 2) { |
| | | if (!hasRenderableBoundary()) { |
| | | fitBoundsToView(bounds); |
| | | } |
| | | visualizationPanel.repaint(); |
| | |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void beginHandheldBoundaryPreview() { |
| | | handheldBoundaryPreviewActive = true; |
| | | handheldBoundaryPreview.clear(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void addHandheldBoundaryPoint(double x, double y) { |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | return; |
| | | } |
| | | if (!handheldBoundaryPreviewActive) { |
| | | beginHandheldBoundaryPreview(); |
| | | } |
| | | Point2D.Double last = handheldBoundaryPreview.isEmpty() ? null : handheldBoundaryPreview.get(handheldBoundaryPreview.size() - 1); |
| | | if (last != null) { |
| | | double dx = x - last.x; |
| | | double dy = y - last.y; |
| | | if (Math.hypot(dx, dy) < 1e-6) { |
| | | visualizationPanel.repaint(); |
| | | return; |
| | | } |
| | | } |
| | | handheldBoundaryPreview.add(new Point2D.Double(x, y)); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void clearHandheldBoundaryPreview() { |
| | | handheldBoundaryPreviewActive = false; |
| | | handheldBoundaryPreview.clear(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public List<Point2D.Double> getHandheldBoundaryPreviewPoints() { |
| | | return new ArrayList<>(handheldBoundaryPreview); |
| | | } |
| | | |
| | | private List<Point2D.Double> parseBoundary(String boundaryCoordinates) { |
| | | List<Point2D.Double> points = new ArrayList<>(); |
| | | String[] entries = boundaryCoordinates.split(";"); |