| | |
| | | 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; |
| | |
| | | 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 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(); |
| | | } |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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(";"); |