| | |
| | | # å²èæºå°åéç¢ç©é
ç½®æä»¶ |
| | | # çææ¶é´ï¼2025-12-05T15:37:09.106292500 |
| | | # çææ¶é´ï¼2025-12-09T11:53:37.128295200 |
| | | # åæ ç³»ï¼WGS84ï¼åº¦åæ ¼å¼ï¼ |
| | | |
| | | # ============ å°ååºåç«é
ç½® ============ |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].baseStation=[ç»åº¦],[N/S],[纬度],[E/W] |
| | | plot.DK-001.baseStation=3949.902389,N,11616.756920,E |
| | | plot.LAND1.baseStation=3949.902389,N,11616.756920,E |
| | | |
| | | # ============ éç¢ç©é
ç½® ============ |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].obstacle.[éç¢ç©åç§°].shape=[0|1] |
| | |
| | | #Base station properties |
| | | #Sat Nov 29 15:05:51 CST 2025 |
| | | #Tue Dec 09 19:03:31 CST 2025 |
| | | dataUpdateTime=-1 |
| | | deviceActivationTime=-1 |
| | | deviceId=1234 |
| | | deviceId=4567 |
| | | installationCoordinates=3949.90238860,N,11616.75692000,E |
| | | iotSimCardNumber=-1 |
| | |
| | | #Dikuai Properties |
| | | #Fri Dec 05 19:16:23 CST 2025 |
| | | DK-001.angleThreshold=-1 |
| | | DK-001.baseStationCoordinates=3949.90238860,N,11616.75692000,E |
| | | DK-001.boundaryCoordinates=-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88 |
| | | DK-001.boundaryOriginalCoordinates=-1 |
| | | DK-001.boundaryPointInterval=-1 |
| | | DK-001.createTime=-1 |
| | | DK-001.intelligentSceneAnalysis=-1 |
| | | DK-001.landArea=300 |
| | | DK-001.landName=åé¢èåª |
| | | DK-001.landNumber=DK-001 |
| | | DK-001.mowingPattern=parallel |
| | | DK-001.mowingTrack=-1 |
| | | DK-001.mowingWidth=100 |
| | | DK-001.plannedPath=1.51,-8.35;0.26,-1.43;-1.35,1.89;0.77,-9.89;-0.22,-10.06;-2.33,1.68 |
| | | DK-001.returnPointCoordinates=-1 |
| | | DK-001.updateTime=2025-12-05 19\:16\:23 |
| | | DK-001.userId=-1 |
| | | #Tue Dec 09 11:53:37 CST 2025 |
| | | LAND1.angleThreshold=-1 |
| | | LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E |
| | | LAND1.boundaryCoordinates=1.31,-9.59;1.86,-11.61;3.12,-12.49;5.50,-12.06;5.95,-10.88;4.97,-3.86;3.16,-0.87;2.79,-2.61;2.42,-4.35;2.05,-6.10;1.68,-7.84;1.31,-9.59 |
| | | LAND1.boundaryOriginalCoordinates=39.831620,116.279297,39.70;39.831618,116.279298,39.80;39.831616,116.279298,39.70;39.831614,116.279298,39.70;39.831612,116.279299,39.70;39.831610,116.279299,39.70;39.831608,116.279300,39.70;39.831606,116.279301,39.70;39.831604,116.279303,39.70;39.831602,116.279304,39.70;39.831600,116.279306,39.70;39.831598,116.279307,39.70;39.831598,116.279309,39.70;39.831597,116.279312,39.60;39.831596,116.279314,39.60;39.831595,116.279317,39.60;39.831594,116.279319,39.60;39.831594,116.279320,39.60;39.831594,116.279323,39.60;39.831595,116.279326,39.60;39.831595,116.279329,39.60;39.831595,116.279331,39.70;39.831595,116.279334,39.70;39.831596,116.279337,39.60;39.831596,116.279339,39.60;39.831596,116.279342,39.60;39.831597,116.279344,39.60;39.831598,116.279346,39.70;39.831600,116.279348,39.70;39.831601,116.279349,39.70;39.831603,116.279350,39.80;39.831605,116.279350,39.70;39.831606,116.279351,39.70;39.831609,116.279352,39.80;39.831611,116.279352,39.70;39.831613,116.279352,39.70;39.831615,116.279352,39.70;39.831617,116.279353,39.70;39.831619,116.279353,39.70;39.831621,116.279353,39.80;39.831623,116.279353,39.80;39.831625,116.279353,39.80;39.831627,116.279353,39.80;39.831629,116.279352,39.80;39.831631,116.279352,39.80;39.831634,116.279351,39.70;39.831636,116.279351,39.70;39.831637,116.279350,39.70;39.831639,116.279350,39.80;39.831641,116.279349,39.70;39.831643,116.279348,39.70;39.831645,116.279348,39.70;39.831647,116.279347,39.70;39.831649,116.279346,39.80;39.831651,116.279346,39.80;39.831653,116.279345,39.80;39.831655,116.279345,39.80;39.831657,116.279344,39.80;39.831659,116.279343,39.70;39.831661,116.279343,39.70;39.831663,116.279342,39.70;39.831665,116.279342,39.70;39.831667,116.279341,39.70;39.831670,116.279341,39.70;39.831672,116.279340,39.70;39.831674,116.279339,39.80;39.831676,116.279338,39.80;39.831678,116.279337,39.80;39.831679,116.279336,39.70;39.831680,116.279334,39.70;39.831681,116.279332,39.70;39.831683,116.279331,39.70;39.831684,116.279329,39.70;39.831686,116.279327,39.70;39.831687,116.279325,39.70;39.831689,116.279323,39.70;39.831691,116.279322,39.70;39.831693,116.279321,39.70;39.831694,116.279320,39.70;39.831696,116.279319,39.70;39.831699,116.279319,39.70 |
| | | LAND1.boundaryPointInterval=-1 |
| | | LAND1.createTime=2025-12-09 11\:16\:40 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.landArea=35.87 |
| | | LAND1.landName=1234 |
| | | LAND1.landNumber=LAND1 |
| | | LAND1.mowingPattern=èºæå¼ |
| | | LAND1.mowingTrack=-1 |
| | | LAND1.mowingWidth=40 |
| | | LAND1.plannedPath=1.88,-7.88;2.62,-4.39;3.25,-1.41;4.78,-3.93;5.74,-10.86;5.35,-11.88;3.17,-12.28;2.03,-11.49;1.52,-9.58;1.88,-7.88;2.27,-7.96;3.01,-4.47;3.43,-2.48;4.39,-4.07;5.33,-10.81;5.06,-11.53;3.26,-11.86;2.38,-11.24;1.93,-9.57;2.27,-7.96;2.66,-8.05;3.40,-4.56;3.61,-3.55;4.01,-4.20;4.92,-10.76;4.77,-11.18;3.35,-11.43;2.73,-11.00;2.34,-9.56;2.66,-8.05;3.05,-8.13;3.71,-4.99;4.51,-10.72;4.47,-10.82;3.44,-11.01;3.08,-10.75;2.75,-9.55;3.05,-8.13;3.44,-8.21;3.63,-7.30;4.08,-10.49;3.54,-10.59;3.43,-10.51;3.16,-9.54;3.44,-8.21 |
| | | LAND1.returnPointCoordinates=-1 |
| | | LAND1.updateTime=2025-12-09 11\:53\:37 |
| | | LAND1.userId=-1 |
| | |
| | | #Mower Configuration Properties - Updated |
| | | #Fri Dec 05 14:26:38 CST 2025 |
| | | #Current work land selection updated |
| | | #Tue Dec 09 19:35:30 CST 2025 |
| | | appVersion=-1 |
| | | currentWorkLandNumber=LAND1 |
| | | cuttingWidth=200 |
| | | firmwareVersion=-1 |
| | | handheldMarkerId=2354 |
| | | handheldMarkerId= |
| | | idleTrailDurationSeconds=60 |
| | | mowerId=1872 |
| | | mowerId=1234 |
| | | serialAutoConnect=true |
| | | serialBaudRate=921600 |
| | | serialPortName=COM15 |
| | |
| | | |
| | | public class TestPlanner { |
| | | public static void main(String[] args) { |
| | | String polygon = "-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88"; |
| | | String polygon = "-3.74,1.88;-0.86,2.51;-0.32,0.12;1.15,-1.75;2.31,-7.15;1.56,-10.27;-0.45,-10.60;-1.73,-9.94;-3.61,0.51;-3.74,1.88"; |
| | | String obstacles = ""; |
| | | String widthMeters = "0.5"; |
| | | String mode = "parallel"; |
| | | String widthMeters = "0.5"; |
| | | String mode = "parallel"; |
| | | |
| | | String pathCoords = Lunjingguihua.generatePathFromStrings(polygon, obstacles, widthMeters, mode); |
| | | System.out.println(pathCoords); |
| | |
| | | import java.io.IOException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.Locale; |
| | | import java.util.Properties; |
| | | |
| | | public class BaseStationDialog extends JDialog { |
| | |
| | | baseStation.load(); |
| | | |
| | | String currentRaw = baseStation.getInstallationCoordinates(); |
| | | String normalizedCurrent = currentRaw == null ? "-1" : currentRaw.trim(); |
| | | if (normalizedCurrent.isEmpty()) { |
| | | normalizedCurrent = "-1"; |
| | | } |
| | | String normalizedCurrent = canonicalizeCoordinateValue(currentRaw); |
| | | boolean hasExistingCoordinate = normalizedCurrent != null && !"-1".equals(normalizedCurrent); |
| | | |
| | | JTextField inputField = new JTextField("-1".equals(normalizedCurrent) ? "" : normalizedCurrent); |
| | | JTextField inputField = new JTextField(hasExistingCoordinate ? normalizedCurrent : ""); |
| | | inputField.setColumns(28); |
| | | |
| | | JPanel dialogPanel = new JPanel(new BorderLayout(0, 8)); |
| | | JLabel hintLabel = new JLabel("请è¾å
¥æ°çåºåç«åæ ï¼æ ¼å¼ç¤ºä¾ï¼2324.194945,N,11330.938547,Eï¼"); |
| | | JLabel hintLabel = new JLabel("请è¾å
¥æ°çåºåç«åæ ï¼ç¤ºä¾ï¼3949.90238860,N,11616.75692000,Eï¼"); |
| | | hintLabel.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | dialogPanel.add(hintLabel, BorderLayout.NORTH); |
| | | dialogPanel.add(inputField, BorderLayout.CENTER); |
| | | |
| | | JOptionPane optionPane = new JOptionPane(dialogPanel, |
| | | JOptionPane.PLAIN_MESSAGE, |
| | | JOptionPane.OK_CANCEL_OPTION); |
| | | JDialog dialog = optionPane.createDialog(this, "ä¿®æ¹åºåç«ä½ç½®"); |
| | | dialog.setModal(true); |
| | | dialog.pack(); |
| | | JOptionPane optionPane = new JOptionPane(dialogPanel, |
| | | JOptionPane.PLAIN_MESSAGE, |
| | | JOptionPane.OK_CANCEL_OPTION); |
| | | JDialog dialog = optionPane.createDialog(this, "ä¿®æ¹åºåç«ä½ç½®"); |
| | | dialog.setModal(true); |
| | | dialog.pack(); |
| | | |
| | | Dimension packedSize = dialog.getSize(); |
| | | Window ownerWindow = getOwner(); |
| | |
| | | referenceWidth = 400; |
| | | } |
| | | dialog.setSize(new Dimension(referenceWidth, packedSize.height)); |
| | | dialog.setResizable(false); |
| | | dialog.setLocationRelativeTo(this); |
| | | dialog.setVisible(true); |
| | | |
| | | Object selectedValue = optionPane.getValue(); |
| | | if (!(selectedValue instanceof Integer) || ((Integer) selectedValue) != JOptionPane.OK_OPTION) { |
| | | dialog.setResizable(false); |
| | | dialog.setLocationRelativeTo(this); |
| | | dialog.setVisible(true); |
| | | Object selectedValue = optionPane.getValue(); |
| | | if (!(selectedValue instanceof Integer) || ((Integer) selectedValue) != JOptionPane.OK_OPTION) { |
| | | return; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | String valueToSave = trimmed.isEmpty() ? "-1" : trimmed; |
| | | String valueToSave = trimmed.isEmpty() ? "-1" : canonicalizeCoordinateValue(trimmed); |
| | | |
| | | if (!"-1".equals(valueToSave) && !looksLikeCoordinateFormat(valueToSave)) { |
| | | int confirm = JOptionPane.showConfirmDialog(this, |
| | |
| | | } |
| | | } |
| | | |
| | | if (valueToSave.equals(normalizedCurrent)) { |
| | | if (normalizedCurrent.equals(valueToSave)) { |
| | | JOptionPane.showMessageDialog(this, |
| | | "åºåç«ä½ç½®æªåçååã", |
| | | "æç¤º", |
| | |
| | | return "è®¾å¤æ²¡æå®è£
åºå®"; |
| | | } |
| | | String coordinates = baseStation.getInstallationCoordinates(); |
| | | if (coordinates == null) { |
| | | String canonical = canonicalizeCoordinateValue(coordinates); |
| | | if (canonical == null || "-1".equals(canonical)) { |
| | | return "è®¾å¤æ²¡æå®è£
åºå®"; |
| | | } |
| | | String trimmed = coordinates.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return "è®¾å¤æ²¡æå®è£
åºå®"; |
| | | } |
| | | try { |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length == 4) { |
| | | String lat = formatCoordinate(parts[0].trim(), true); |
| | | String latDir = parts[1].trim(); |
| | | String lon = formatCoordinate(parts[2].trim(), false); |
| | | String lonDir = parts[3].trim(); |
| | | return String.format("%s°%s, %s°%s", lat, latDir, lon, lonDir); |
| | | } |
| | | } catch (Exception e) { |
| | | // ignore formatting errors and fall back to raw value |
| | | } |
| | | return trimmed; |
| | | return canonical; |
| | | } |
| | | |
| | | private String getSimCardDisplay() { |
| | |
| | | return !trimmed.isEmpty() && !"-1".equals(trimmed); |
| | | } |
| | | |
| | | private String formatCoordinate(String coordinate, boolean isLatitude) { |
| | | // æ ¼å¼ååæ ï¼2324.194945 -> 23°24.1949' |
| | | try { |
| | | // åº¦æ°æ¯å2ä½ï¼çº¬åº¦ï¼æ3ä½ï¼ç»åº¦ï¼ |
| | | int degreeDigits = isLatitude ? 2 : 3; |
| | | String degreeStr = coordinate.substring(0, degreeDigits); |
| | | String minuteStr = coordinate.substring(degreeDigits); |
| | | |
| | | // ä¿ç4ä½å°æ° |
| | | double minutes = Double.parseDouble(minuteStr); |
| | | String formattedMinutes = String.format("%.4f", minutes); |
| | | |
| | | return degreeStr + "°" + formattedMinutes + "'"; |
| | | } catch (Exception e) { |
| | | return coordinate; |
| | | // Normalizes coordinate strings into degree-minute format when possible for consistent display/storage |
| | | private String canonicalizeCoordinateValue(String value) { |
| | | if (value == null) { |
| | | return "-1"; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return "-1"; |
| | | } |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length != 4) { |
| | | return trimmed; |
| | | } |
| | | String latDirRaw = parts[1] == null ? "" : parts[1].trim(); |
| | | String lonDirRaw = parts[3] == null ? "" : parts[3].trim(); |
| | | if (latDirRaw.isEmpty() || lonDirRaw.isEmpty()) { |
| | | return trimmed; |
| | | } |
| | | String latDir = latDirRaw.toUpperCase(Locale.ROOT); |
| | | String lonDir = lonDirRaw.toUpperCase(Locale.ROOT); |
| | | if (!isValidDirection(latDir, true) || !isValidDirection(lonDir, false)) { |
| | | return trimmed; |
| | | } |
| | | String formattedLat = convertToDegreeMinuteString(parts[0], latDir, true); |
| | | String formattedLon = convertToDegreeMinuteString(parts[2], lonDir, false); |
| | | if (formattedLat == null || formattedLon == null) { |
| | | return trimmed; |
| | | } |
| | | return formattedLat + "," + latDir + "," + formattedLon + "," + lonDir; |
| | | } |
| | | |
| | | private boolean isValidDirection(String direction, boolean isLatitude) { |
| | | if (direction == null) { |
| | | return false; |
| | | } |
| | | if (isLatitude) { |
| | | return "N".equals(direction) || "S".equals(direction); |
| | | } |
| | | return "E".equals(direction) || "W".equals(direction); |
| | | } |
| | | |
| | | // Converts decimal degrees or degree-minute input into a canonical degree-minute string (8 decimal places) |
| | | private String convertToDegreeMinuteString(String rawValue, String direction, boolean isLatitude) { |
| | | double decimal = parseCoordinateToDecimalDegrees(rawValue, direction, isLatitude); |
| | | if (!Double.isFinite(decimal)) { |
| | | return null; |
| | | } |
| | | double absDecimal = Math.abs(decimal); |
| | | int degrees = (int) Math.floor(absDecimal); |
| | | double minutes = (absDecimal - degrees) * 60.0d; |
| | | double degreeMinutes = degrees * 100.0d + minutes; |
| | | return String.format(Locale.US, "%.8f", degreeMinutes); |
| | | } |
| | | |
| | | private double parseCoordinateToDecimalDegrees(String rawValue, String direction, boolean isLatitude) { |
| | | if (rawValue == null) { |
| | | return Double.NaN; |
| | | } |
| | | String trimmed = rawValue.trim(); |
| | | if (trimmed.isEmpty()) { |
| | | return Double.NaN; |
| | | } |
| | | double numeric; |
| | | try { |
| | | numeric = Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | |
| | | double abs = Math.abs(numeric); |
| | | double boundary = isLatitude ? 90d : 180d; |
| | | double decimal; |
| | | if (abs <= boundary) { |
| | | decimal = abs; |
| | | } else { |
| | | double degrees = Math.floor(abs / 100d); |
| | | double minutes = abs - degrees * 100d; |
| | | decimal = degrees + minutes / 60d; |
| | | } |
| | | |
| | | String dirUpper = direction == null ? "" : direction.trim().toUpperCase(Locale.ROOT); |
| | | if ("S".equals(dirUpper) || "W".equals(dirUpper)) { |
| | | decimal = -decimal; |
| | | } else if (!"N".equals(dirUpper) && !"E".equals(dirUpper) && numeric < 0d) { |
| | | decimal = -decimal; |
| | | } |
| | | return decimal; |
| | | } |
| | | |
| | | private void lockBaseStationPosition() { |
| | |
| | | JOptionPane.INFORMATION_MESSAGE); |
| | | |
| | | // æ´æ°åºåç«ä½ç½®ï¼è¿éä½¿ç¨æ¨¡ææ°æ®ï¼å®é
åºä»GPSè·åï¼ |
| | | String newPosition = "2324.194945,N,11330.938547,E"; |
| | | String newPosition = canonicalizeCoordinateValue("2324.194945,N,11330.938547,E"); |
| | | String timestamp = String.valueOf(System.currentTimeMillis()); |
| | | |
| | | if (!hasBaseStationId()) { |
| | |
| | | if (!optimized.get(0).equals(optimized.get(optimized.size() - 1))) { |
| | | Coordinate first = optimized.get(0); |
| | | Coordinate last = optimized.get(optimized.size() - 1); |
| | | double closingDistance = calculateDistance(first, last); |
| | | |
| | | double closingDistance = calculateDistance(last, first); |
| | | |
| | | if (closingDistance > params.interval) { |
| | | // 妿é¦å°¾è·ç¦»è¾è¿ï¼æ·»å ä¸é´ç¹ |
| | | List<Coordinate> interpolated = interpolateBoundary( |
| | | List.of(last, first), params.interval |
| | | ); |
| | | optimized.addAll(interpolated.subList(1, interpolated.size() - 1)); |
| | | int segments = (int) Math.ceil(closingDistance / params.interval); |
| | | for (int i = 1; i < segments; i++) { |
| | | double ratio = (double) i / segments; |
| | | Coordinate interpolatedPoint = interpolate(last, first, ratio); |
| | | if (!interpolatedPoint.equals(last)) { |
| | | optimized.add(interpolatedPoint); |
| | | } |
| | | } |
| | | } |
| | | optimized.add(first); |
| | | } |
| | | |
| | | return optimized; |
| | | |
| | | return removeConsecutiveDuplicates(optimized); |
| | | } |
| | | |
| | | // éæ ¼ææ¯-æ®å
ç®æ³ |
| | |
| | | return interpolated; |
| | | } |
| | | |
| | | private List<Coordinate> removeConsecutiveDuplicates(List<Coordinate> points) { |
| | | if (points == null || points.isEmpty()) { |
| | | return points; |
| | | } |
| | | |
| | | List<Coordinate> cleaned = new ArrayList<>(); |
| | | Coordinate previous = null; |
| | | for (Coordinate point : points) { |
| | | if (previous == null || !point.equals(previous)) { |
| | | cleaned.add(point); |
| | | previous = point; |
| | | } |
| | | } |
| | | |
| | | if (!cleaned.isEmpty() && !cleaned.get(0).equals(cleaned.get(cleaned.size() - 1))) { |
| | | cleaned.add(cleaned.get(0)); |
| | | } |
| | | return cleaned; |
| | | } |
| | | |
| | | // çº¿æ§æå¼ |
| | | private Coordinate interpolate(Coordinate p1, Coordinate p2, double ratio) { |
| | | double x = p1.x + (p2.x - p1.x) * ratio; |
| | |
| | | double baseLat = parseDMToDecimal(baseParts[0], baseParts[1]); |
| | | double baseLon = parseDMToDecimal(baseParts[2], baseParts[3]); |
| | | |
| | | // å°Coordinateå表转æ¢ä¸ºå±é¨åæ ç³»åæ |
| | | List<BoundaryAlgorithm.Coordinate> localCoordinates = |
| | | convertToLocalCoordinates(coordinates, baseLat, baseLon); |
| | | // å°Coordinateå表转æ¢ä¸ºå±é¨åæ ç³»åæ |
| | | List<BoundaryAlgorithm.Coordinate> localCoordinates = |
| | | convertToLocalCoordinates(coordinates, baseLat, baseLon); |
| | | |
| | | // ä¸è§å½¢å°åºåç¹æ®å¤çï¼é¿å
è¿åº¦æå¼å¯¼è´ç¹æ°æ©å¢ |
| | | if (localCoordinates.size() == 3) { |
| | | double triangleArea = calculatePolygonArea(localCoordinates); |
| | | double trianglePerimeter = calculatePerimeter(localCoordinates); |
| | | |
| | | System.out.println("æ£æµå°ä¸è§å½¢è¾¹çï¼é¢ç§¯=" + String.format("%.2f", triangleArea) + |
| | | "m², å¨é¿=" + String.format("%.2f", trianglePerimeter) + "m"); |
| | | |
| | | if (triangleArea < 100.0 || trianglePerimeter < 30.0) { |
| | | System.out.println("å°ä¸è§å½¢ï¼è·³è¿æå¼ä¼å"); |
| | | BoundaryAlgorithm.Coordinate firstPoint = localCoordinates.get(0); |
| | | List<BoundaryAlgorithm.Coordinate> trianglePoints = new ArrayList<>(localCoordinates); |
| | | trianglePoints.add(new BoundaryAlgorithm.Coordinate( |
| | | firstPoint.x, |
| | | firstPoint.y, |
| | | firstPoint.lat, |
| | | firstPoint.lon)); |
| | | return convertBoundaryPointsToString(trianglePoints); |
| | | } |
| | | } |
| | | |
| | | // åå»ºç®æ³å®ä¾ |
| | | BoundaryAlgorithm algorithm = new BoundaryAlgorithm(); |
| | |
| | | |
| | | return Math.abs(area) / 2.0; |
| | | } |
| | | |
| | | private static double calculatePerimeter(List<BoundaryAlgorithm.Coordinate> points) { |
| | | if (points == null || points.size() != 3) { |
| | | return 0.0; |
| | | } |
| | | double d1 = calculateDistance(points.get(0), points.get(1)); |
| | | double d2 = calculateDistance(points.get(1), points.get(2)); |
| | | double d3 = calculateDistance(points.get(2), points.get(0)); |
| | | return d1 + d2 + d3; |
| | | } |
| | | |
| | | // ============ å
¶ä»æ¹æ³ä¿æä¸å ============ |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package chuankou; |
| | | |
| | | import java.io.File; |
| | | import java.net.URISyntaxException; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | |
| | | /** |
| | | * Ensures the correct jSerialComm native library is available on Windows/x86_64 systems. |
| | | */ |
| | | public final class SerialPortNativeLoader { |
| | | private static final String LIB_PROPERTY = "com.fazecast.jSerialComm.library.path"; |
| | | private static final String EXPECTED_DLL = "jSerialComm.dll"; |
| | | |
| | | private SerialPortNativeLoader() { |
| | | // utility class |
| | | } |
| | | |
| | | public static void ensureLibraryPresent() { |
| | | if (System.getProperty(LIB_PROPERTY) != null) { |
| | | return; |
| | | } |
| | | |
| | | String osName = System.getProperty("os.name", "").toLowerCase(); |
| | | if (!osName.contains("win")) { |
| | | return; |
| | | } |
| | | |
| | | String arch = System.getProperty("os.arch", "").toLowerCase(); |
| | | if (!arch.contains("64")) { |
| | | return; |
| | | } |
| | | |
| | | Path candidateDir = Paths.get("lib", "native", "windows", "x86_64").toAbsolutePath(); |
| | | File dllFile = candidateDir.resolve(EXPECTED_DLL).toFile(); |
| | | if (!dllFile.isFile()) { |
| | | candidateDir = resolveFromCodeSource(); |
| | | if (candidateDir != null) { |
| | | dllFile = candidateDir.resolve(EXPECTED_DLL).toFile(); |
| | | } |
| | | } |
| | | |
| | | if (dllFile.isFile()) { |
| | | System.setProperty(LIB_PROPERTY, candidateDir.toString()); |
| | | } else { |
| | | System.err.println("Expected jSerialComm native library not found. Checked " + dllFile); |
| | | } |
| | | } |
| | | |
| | | private static Path resolveFromCodeSource() { |
| | | try { |
| | | java.security.CodeSource codeSource = SerialPortNativeLoader.class.getProtectionDomain().getCodeSource(); |
| | | if (codeSource == null) { |
| | | return null; |
| | | } |
| | | |
| | | Path location = Paths.get(codeSource.getLocation().toURI()).toAbsolutePath(); |
| | | Path baseDir = location.getParent(); |
| | | if (baseDir == null) { |
| | | return null; |
| | | } |
| | | |
| | | Path siblingLibDir = baseDir.resolveSibling("lib").resolve("native").resolve("windows").resolve("x86_64"); |
| | | if (siblingLibDir.toFile().isDirectory()) { |
| | | return siblingLibDir; |
| | | } |
| | | |
| | | Path relativeLibDir = baseDir.resolve("lib").resolve("native").resolve("windows").resolve("x86_64"); |
| | | if (relativeLibDir.toFile().isDirectory()) { |
| | | return relativeLibDir; |
| | | } |
| | | } catch (URISyntaxException | SecurityException ignored) { |
| | | // ignore |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | |
| | | package chuankou; |
| | | |
| | | import com.fazecast.jSerialComm.SerialPort; |
| | | import java.util.function.Consumer; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.util.function.Consumer; |
| | | |
| | | public class SerialPortService { |
| | | |
| | | static { |
| | | SerialPortNativeLoader.ensureLibraryPresent(); |
| | | } |
| | | |
| | | private SerialPort port; |
| | | private volatile boolean capturing = false; |
| | | private volatile boolean paused = true; |
| | |
| | | package denglu; |
| | | |
| | | import homein.WenJianSuo; |
| | | import ui.UIConfig; |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | |
| | | |
| | | private void initializeTranslations() { |
| | | translations = new HashMap<>(); |
| | | |
| | | |
| | | // ä¸æç¿»è¯ |
| | | Map<String, String> zh = new HashMap<>(); |
| | | zh.put("username_label", "ç¨æ·åæé®ç®±"); |
| | |
| | | zh.put("login_button", "ç»å½"); |
| | | zh.put("no_account", "è¿æ²¡æè´¦æ·ï¼"); |
| | | zh.put("sign_up", "ç«å³æ³¨å"); |
| | | zh.put("app_info", "AutoMow © 2023 - æºè½å²èè§£å³æ¹æ¡"); |
| | | zh.put("app_title", "æºè½å²èç³»ç»"); |
| | | zh.put("window_title", "æºè½å²èç³»ç» - ç»å½"); |
| | | zh.put("app_info", "æºè½å²èç³»ç» Â© 2023 - æºè½å²èè§£å³æ¹æ¡"); |
| | | zh.put("register_button", "æ³¨åæ°è´¦æ·"); |
| | | translations.put("zh", zh); |
| | | |
| | |
| | | en.put("login_button", "Sign In"); |
| | | en.put("no_account", "Don't have an account?"); |
| | | en.put("sign_up", "Sign Up"); |
| | | en.put("app_info", "AutoMow © 2023 - Smart Mowing Solutions"); |
| | | en.put("app_title", "Smart Mowing System"); |
| | | en.put("window_title", "Smart Mowing System - Login"); |
| | | en.put("app_info", "Smart Mowing System © 2023 - Intelligent Lawn Care"); |
| | | en.put("register_button", "Create New Account"); |
| | | translations.put("en", en); |
| | | |
| | |
| | | es.put("login_button", "Iniciar Sesión"); |
| | | es.put("no_account", "¿No tienes una cuenta?"); |
| | | es.put("sign_up", "RegÃstrate"); |
| | | es.put("app_info", "AutoMow © 2023 - Soluciones de Corte Inteligente"); |
| | | es.put("app_title", "Sistema de Corte Inteligente"); |
| | | es.put("window_title", "Sistema de Corte Inteligente - Iniciar Sesión"); |
| | | es.put("app_info", "Sistema de Corte Inteligente © 2023 - Soluciones Inteligentes"); |
| | | es.put("register_button", "Crear Nueva Cuenta"); |
| | | translations.put("es", es); |
| | | |
| | |
| | | fr.put("login_button", "Se connecter"); |
| | | fr.put("no_account", "Vous n'avez pas de compte?"); |
| | | fr.put("sign_up", "S'inscrire"); |
| | | fr.put("app_info", "AutoMow © 2023 - Solutions de Tonte Intelligente"); |
| | | es.put("register_button", "Créer un Nouveau Compte"); |
| | | fr.put("app_title", "Système de Tonte Intelligent"); |
| | | fr.put("window_title", "Système de Tonte Intelligent - Connexion"); |
| | | fr.put("app_info", "Système de Tonte Intelligent © 2023 - Solutions de Tonte"); |
| | | fr.put("register_button", "Créer un Nouveau Compte"); |
| | | translations.put("fr", fr); |
| | | |
| | | // å¾·è¯ç¿»è¯ |
| | |
| | | de.put("login_button", "Anmelden"); |
| | | de.put("no_account", "Sie haben noch kein Konto?"); |
| | | de.put("sign_up", "Registrieren"); |
| | | de.put("app_info", "AutoMow © 2023 - Intelligente Mäh-Lösungen"); |
| | | de.put("app_title", "Intelligentes Mähsystem"); |
| | | de.put("window_title", "Intelligentes Mähsystem - Anmeldung"); |
| | | de.put("app_info", "Intelligentes Mähsystem © 2023 - Mählösungen"); |
| | | de.put("register_button", "Neues Konto Erstellen"); |
| | | translations.put("de", de); |
| | | } |
| | | |
| | | private void initializeUI() { |
| | | setTitle("AutoMow - ç»å½"); |
| | | setTitle(getTranslationValue(currentLanguageCode, "window_title", "ç³»ç»ç»å½")); |
| | | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); |
| | | |
| | | // 设置紧åå¸å±å°ºå¯¸ |
| | | setSize(320, 520); |
| | | setMinimumSize(new Dimension(300, 500)); |
| | | setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); |
| | | setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT)); |
| | | setResizable(false); |
| | | |
| | | setupCompactLayout(); |
| | |
| | | )); |
| | | |
| | | // ç»å½æ é¢ |
| | | JLabel loginTitle = new JLabel("AutoMow", SwingConstants.CENTER); |
| | | loginTitle.setFont(new Font("PingFang SC", Font.BOLD, 24)); |
| | | loginTitle.setForeground(THEME_COLOR); |
| | | loginTitle.setAlignmentX(Component.CENTER_ALIGNMENT); |
| | | |
| | | // ç¨æ·åè¾å
¥ |
| | | usernameLabel = new JLabel("ç¨æ·åæé®ç®±", SwingConstants.CENTER); |
| | | usernameLabel.setFont(new Font("PingFang SC", Font.BOLD, 13)); |
| | |
| | | rememberForgotPanel.add(rememberMe, BorderLayout.WEST); |
| | | rememberForgotPanel.add(forgotPassword, BorderLayout.EAST); |
| | | |
| | | // ç»å½æé® - æ°´å¹³å±
ä¸ï¼é¿åº¦ä¸ææ¬æ¡ç¸å |
| | | // ç»å½æé® - æ°´å¹³å±
ä¸ï¼é¿åº¦ä¸ææ¬æ¡å®½åº¦ä¿æç¸è¿ |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); |
| | | buttonPanel.setBackground(Color.WHITE); |
| | | buttonPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50)); |
| | |
| | | loginButton.setBackground(THEME_COLOR); |
| | | loginButton.setForeground(Color.WHITE); |
| | | loginButton.setFont(new Font("PingFang SC", Font.BOLD, 15)); |
| | | // 设置ç»å½æé®é¿åº¦ä¸ææ¬æ¡ç¸å |
| | | loginButton.setPreferredSize(new Dimension(260, 42)); |
| | | // 设置ç»å½æé®é¿åº¦æ¥è¿ææ¬æ¡å®½åº¦ |
| | | loginButton.setPreferredSize(new Dimension(300, 48)); |
| | | loginButton.setBorderPainted(false); |
| | | loginButton.setFocusPainted(false); |
| | | loginButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | |
| | | registerButton.setBackground(Color.WHITE); |
| | | registerButton.setForeground(THEME_COLOR); |
| | | registerButton.setFont(new Font("PingFang SC", Font.BOLD, 14)); |
| | | registerButton.setPreferredSize(new Dimension(140, 38)); |
| | | registerButton.setPreferredSize(new Dimension(180, 42)); |
| | | registerButton.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(THEME_COLOR), |
| | | BorderFactory.createEmptyBorder(5, 15, 5, 15) |
| | |
| | | signupPanel.add(signupLink); |
| | | |
| | | // ç»è£
表å |
| | | formPanel.add(loginTitle); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 25))); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | formPanel.add(usernameLabel); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 5))); |
| | | formPanel.add(usernameField); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 30))); |
| | | formPanel.add(passwordLabel); |
| | | formPanel.add(Box.createRigidArea(new Dimension(0, 5))); |
| | | formPanel.add(passwordField); |
| | |
| | | JPanel appInfoPanel = new JPanel(); |
| | | appInfoPanel.setBackground(Color.WHITE); |
| | | appInfoPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 5, 0)); |
| | | appInfoLabel = new JLabel("AutoMow © 2023 - æºè½å²èè§£å³æ¹æ¡"); |
| | | appInfoLabel = new JLabel( |
| | | getTranslationValue(currentLanguageCode, "app_info", "æºè½å²èç³»ç» Â© 2023") |
| | | ); |
| | | appInfoLabel.setFont(new Font("PingFang SC", Font.PLAIN, 10)); |
| | | appInfoLabel.setForeground(new Color(150, 150, 150)); |
| | | appInfoPanel.add(appInfoLabel); |
| | |
| | | |
| | | if (translation != null) { |
| | | // æ´æ°ææçé¢ææ¬ |
| | | usernameLabel.setText(translation.get("username_label")); |
| | | passwordLabel.setText(translation.get("password_label")); |
| | | rememberMe.setText(translation.get("remember_me")); |
| | | forgotPassword.setText(translation.get("forgot_password")); |
| | | loginButton.setText(translation.get("login_button")); |
| | | noAccountLabel.setText(translation.get("no_account")); |
| | | signupLink.setText(translation.get("sign_up")); |
| | | appInfoLabel.setText(translation.get("app_info")); |
| | | |
| | | usernameLabel.setText(getTranslationValue(langCode, "username_label", usernameLabel.getText())); |
| | | passwordLabel.setText(getTranslationValue(langCode, "password_label", passwordLabel.getText())); |
| | | rememberMe.setText(getTranslationValue(langCode, "remember_me", rememberMe.getText())); |
| | | forgotPassword.setText(getTranslationValue(langCode, "forgot_password", forgotPassword.getText())); |
| | | loginButton.setText(getTranslationValue(langCode, "login_button", loginButton.getText())); |
| | | noAccountLabel.setText(getTranslationValue(langCode, "no_account", noAccountLabel.getText())); |
| | | signupLink.setText(getTranslationValue(langCode, "sign_up", signupLink.getText())); |
| | | appInfoLabel.setText(getTranslationValue(langCode, "app_info", appInfoLabel.getText())); |
| | | |
| | | // æ´æ°æ³¨åæé®ææ¬ |
| | | registerButton.setText(translation.get("register_button")); |
| | | |
| | | registerButton.setText(getTranslationValue(langCode, "register_button", registerButton.getText())); |
| | | |
| | | // æ´æ°çªå£æ é¢ |
| | | setTitle("AutoMow - " + (langCode.equals("zh") ? "ç»å½" : "Login")); |
| | | setTitle(getTranslationValue(langCode, "window_title", getTitle())); |
| | | |
| | | // æ´æ°è¯è¨è®¾ç½®å°æä»¶ |
| | | updateLanguagePreference(langCode); |
| | |
| | | } |
| | | } |
| | | |
| | | private String getTranslationValue(String languageCode, String key, String defaultValue) { |
| | | Map<String, String> translation = translations.get(languageCode); |
| | | if (translation != null) { |
| | | String value = translation.get(key); |
| | | if (value != null && !value.trim().isEmpty()) { |
| | | return value; |
| | | } |
| | | } |
| | | return defaultValue; |
| | | } |
| | | |
| | | private void updateLanguagePreference(String languageCode) { |
| | | String currentLanguage = UserChuShiHua.getProperty("language"); |
| | | if (!languageCode.equals(currentLanguage)) { |
| | |
| | | RegistrationFrame registerFrame = new RegistrationFrame(this, currentLanguageCode); |
| | | registerFrame.setVisible(true); |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | SwingUtilities.invokeLater(() -> { |
| | | new Denglu().setVisible(true); |
| | | }); |
| | | } |
| | | |
| | | } |
| | |
| | | package denglu; |
| | | |
| | | import ui.UIConfig; |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | |
| | | |
| | | // ä¸æç¿»è¯ |
| | | Map<String, String> zh = new HashMap<>(); |
| | | zh.put("title", "ç¨æ·æ³¨å"); |
| | | zh.put("title", "ç¨æ·æ³¨å"); |
| | | zh.put("window_title", "æºè½å²èç³»ç» - ç¨æ·æ³¨å"); |
| | | zh.put("username_label", "ç¨æ·å"); |
| | | zh.put("email_label", "é®ç®±å°å"); |
| | | zh.put("verification_code_label", "éªè¯ç "); |
| | |
| | | |
| | | // è±æç¿»è¯ |
| | | Map<String, String> en = new HashMap<>(); |
| | | en.put("title", "User Registration"); |
| | | en.put("title", "User Registration"); |
| | | en.put("window_title", "Smart Mowing System - User Registration"); |
| | | en.put("username_label", "Username"); |
| | | en.put("email_label", "Email Address"); |
| | | en.put("verification_code_label", "Verification Code"); |
| | |
| | | |
| | | // 西ççè¯ç¿»è¯ |
| | | Map<String, String> es = new HashMap<>(); |
| | | es.put("title", "Registro de Usuario"); |
| | | es.put("title", "Registro de Usuario"); |
| | | es.put("window_title", "Sistema de Corte Inteligente - Registro de Usuario"); |
| | | es.put("username_label", "Usuario"); |
| | | es.put("email_label", "Correo Electrónico"); |
| | | es.put("verification_code_label", "Código de Verificación"); |
| | |
| | | |
| | | // æ³è¯ç¿»è¯ |
| | | Map<String, String> fr = new HashMap<>(); |
| | | fr.put("title", "Inscription Utilisateur"); |
| | | fr.put("title", "Inscription Utilisateur"); |
| | | fr.put("window_title", "Système de Tonte Intelligent - Inscription"); |
| | | fr.put("username_label", "Nom d'utilisateur"); |
| | | fr.put("email_label", "Adresse Email"); |
| | | fr.put("verification_code_label", "Code de Vérification"); |
| | |
| | | |
| | | // å¾·è¯ç¿»è¯ |
| | | Map<String, String> de = new HashMap<>(); |
| | | de.put("title", "Benutzerregistrierung"); |
| | | de.put("title", "Benutzerregistrierung"); |
| | | de.put("window_title", "Intelligentes Mähsystem - Registrierung"); |
| | | de.put("username_label", "Benutzername"); |
| | | de.put("email_label", "E-Mail-Adresse"); |
| | | de.put("verification_code_label", "Verifizierungscode"); |
| | |
| | | translations.put("de", de); |
| | | } |
| | | |
| | | private String getTranslationValue(String languageCode, String key, String defaultValue) { |
| | | Map<String, String> translation = translations.get(languageCode); |
| | | if (translation != null) { |
| | | String value = translation.get(key); |
| | | if (value != null && !value.trim().isEmpty()) { |
| | | return value; |
| | | } |
| | | } |
| | | return defaultValue; |
| | | } |
| | | |
| | | private void initializeUI() { |
| | | setTitle("AutoMow - ç¨æ·æ³¨å"); |
| | | setTitle(getTranslationValue(currentLanguageCode, "window_title", "ç¨æ·æ³¨å")); |
| | | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); |
| | | setSize(350, 500); |
| | | setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); |
| | | setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT)); |
| | | setLocationRelativeTo(parentFrame); |
| | | setResizable(false); |
| | | |
| | |
| | | |
| | | if (translation != null) { |
| | | // æ´æ°ææçé¢ææ¬ |
| | | setTitle("AutoMow - " + translation.get("title")); |
| | | userLabel.setText(translation.get("username_label")); |
| | | emailLabel.setText(translation.get("email_label")); |
| | | verificationCodeLabel.setText(translation.get("verification_code_label")); |
| | | passLabel.setText(translation.get("password_label")); |
| | | confirmPassLabel.setText(translation.get("confirm_password_label")); |
| | | registerButton.setText(translation.get("register_button")); |
| | | cancelButton.setText(translation.get("cancel_button")); |
| | | sendCodeButton.setText(translation.get("send_code_button")); |
| | | setTitle(getTranslationValue(languageCode, "window_title", getTitle())); |
| | | userLabel.setText(getTranslationValue(languageCode, "username_label", userLabel.getText())); |
| | | emailLabel.setText(getTranslationValue(languageCode, "email_label", emailLabel.getText())); |
| | | verificationCodeLabel.setText(getTranslationValue(languageCode, "verification_code_label", verificationCodeLabel.getText())); |
| | | passLabel.setText(getTranslationValue(languageCode, "password_label", passLabel.getText())); |
| | | confirmPassLabel.setText(getTranslationValue(languageCode, "confirm_password_label", confirmPassLabel.getText())); |
| | | registerButton.setText(getTranslationValue(languageCode, "register_button", registerButton.getText())); |
| | | cancelButton.setText(getTranslationValue(languageCode, "cancel_button", cancelButton.getText())); |
| | | sendCodeButton.setText(getTranslationValue(languageCode, "send_code_button", sendCodeButton.getText())); |
| | | } |
| | | } |
| | | |
| | |
| | | import java.awt.datatransfer.*; |
| | | import java.awt.datatransfer.StringSelection; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.FileOutputStream; |
| | | import java.io.IOException; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import ui.UIConfig; |
| | |
| | | import java.util.List; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Objects; |
| | | import java.util.Properties; |
| | | |
| | | import lujing.Lunjingguihua; |
| | | import zhangaiwu.AddDikuai; |
| | |
| | | private JButton addLandBtn; |
| | | |
| | | private static String currentWorkLandNumber; |
| | | private static final String WORK_LAND_KEY = "currentWorkLandNumber"; |
| | | private static final String PROPERTIES_FILE = "set.properties"; |
| | | private static final Map<String, Boolean> boundaryPointVisibility = new HashMap<>(); |
| | | private ImageIcon workSelectedIcon; |
| | | private ImageIcon workUnselectedIcon; |
| | |
| | | boolean isCurrent = currentWorkLandNumber != null && currentWorkLandNumber.equals(landNumber); |
| | | if (isCurrent) { |
| | | renderer.setBoundaryPointsVisible(desiredState); |
| | | renderer.setBoundaryPointSizeScale(desiredState ? 0.5d : 1.0d); |
| | | } |
| | | } |
| | | } |
| | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setPreferredSize(new Dimension(360, 240)); |
| | | |
| | | int option = JOptionPane.showConfirmDialog( |
| | | this, |
| | | scrollPane, |
| | | title, |
| | | JOptionPane.OK_CANCEL_OPTION, |
| | | JOptionPane.PLAIN_MESSAGE); |
| | | |
| | | if (option == JOptionPane.OK_OPTION) { |
| | | return textArea.getText(); |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | JDialog dialog; |
| | | if (owner instanceof Frame) { |
| | | dialog = new JDialog((Frame) owner, title, true); |
| | | } else if (owner instanceof Dialog) { |
| | | dialog = new JDialog((Dialog) owner, title, true); |
| | | } else { |
| | | dialog = new JDialog((Frame) null, title, true); |
| | | } |
| | | return null; |
| | | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | |
| | | JPanel contentPanel = new JPanel(new BorderLayout()); |
| | | contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); |
| | | contentPanel.add(scrollPane, BorderLayout.CENTER); |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); |
| | | JButton okButton = new JButton("ç¡®å®"); |
| | | JButton cancelButton = new JButton("åæ¶"); |
| | | JButton copyButton = new JButton("å¤å¶"); |
| | | |
| | | final boolean[] confirmed = new boolean[] {false}; |
| | | final String[] resultHolder = new String[1]; |
| | | |
| | | okButton.addActionListener(e -> { |
| | | resultHolder[0] = textArea.getText(); |
| | | confirmed[0] = true; |
| | | dialog.dispose(); |
| | | }); |
| | | |
| | | cancelButton.addActionListener(e -> dialog.dispose()); |
| | | |
| | | copyButton.addActionListener(e -> { |
| | | String text = textArea.getText(); |
| | | if (text == null) { |
| | | text = ""; |
| | | } |
| | | String trimmed = text.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | JOptionPane.showMessageDialog(dialog, title + " æªè®¾ç½®", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | try { |
| | | StringSelection selection = new StringSelection(text); |
| | | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); |
| | | clipboard.setContents(selection, selection); |
| | | JOptionPane.showMessageDialog(dialog, title + " å·²å¤å¶å°åªè´´æ¿", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | } catch (Exception ex) { |
| | | JOptionPane.showMessageDialog(dialog, "å¤å¶å¤±è´¥: " + ex.getMessage(), "é误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | }); |
| | | |
| | | buttonPanel.add(okButton); |
| | | buttonPanel.add(cancelButton); |
| | | buttonPanel.add(copyButton); |
| | | |
| | | contentPanel.add(buttonPanel, BorderLayout.SOUTH); |
| | | dialog.setContentPane(contentPanel); |
| | | dialog.getRootPane().setDefaultButton(okButton); |
| | | dialog.pack(); |
| | | dialog.setLocationRelativeTo(this); |
| | | dialog.setVisible(true); |
| | | |
| | | return confirmed[0] ? resultHolder[0] : null; |
| | | } |
| | | |
| | | private boolean saveFieldAndRefresh(Dikuai dikuai, String fieldName, String value) { |
| | |
| | | switch (trimmed) { |
| | | case "1": |
| | | case "spiral": |
| | | case "èºæ": |
| | | case "èºææ¨¡å¼": |
| | | return "spiral"; |
| | | case "0": |
| | | case "parallel": |
| | | case "å¹³è¡": |
| | | case "å¹³è¡æ¨¡å¼": |
| | | default: |
| | | if (trimmed.contains("èºæ")) { |
| | | return "spiral"; |
| | | } |
| | | if (trimmed.contains("spiral")) { |
| | | return "spiral"; |
| | | } |
| | | if (trimmed.contains("parallel")) { |
| | | return "parallel"; |
| | | } |
| | | if (trimmed.contains("å¹³è¡")) { |
| | | return "parallel"; |
| | | } |
| | | return "parallel"; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | public static void setCurrentWorkLand(String landNumber, String landName) { |
| | | currentWorkLandNumber = landNumber; |
| | | String sanitizedLandNumber = sanitizeLandNumber(landNumber); |
| | | boolean changed = !Objects.equals(currentWorkLandNumber, sanitizedLandNumber); |
| | | if (!changed) { |
| | | String persisted = readPersistedWorkLandNumber(); |
| | | if (!Objects.equals(persisted, sanitizedLandNumber)) { |
| | | changed = true; |
| | | } |
| | | } |
| | | currentWorkLandNumber = sanitizedLandNumber; |
| | | |
| | | Dikuai dikuai = null; |
| | | if (landNumber != null) { |
| | | dikuai = Dikuai.getDikuai(landNumber); |
| | | if (sanitizedLandNumber != null) { |
| | | dikuai = Dikuai.getDikuai(sanitizedLandNumber); |
| | | if (dikuai != null && (landName == null || "-1".equals(landName) || landName.trim().isEmpty())) { |
| | | landName = dikuai.getLandName(); |
| | | } |
| | |
| | | |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | if (landNumber == null) { |
| | | if (sanitizedLandNumber == null) { |
| | | shouye.updateCurrentAreaName(null); |
| | | } else { |
| | | shouye.updateCurrentAreaName(landName); |
| | |
| | | renderer.applyLandMetadata(dikuai); |
| | | String boundary = (dikuai != null) ? dikuai.getBoundaryCoordinates() : null; |
| | | String plannedPath = (dikuai != null) ? dikuai.getPlannedPath() : null; |
| | | List<Obstacledge.Obstacle> configuredObstacles = (landNumber != null) ? loadObstaclesFromConfig(landNumber) : Collections.emptyList(); |
| | | List<Obstacledge.Obstacle> configuredObstacles = (sanitizedLandNumber != null) ? loadObstaclesFromConfig(sanitizedLandNumber) : Collections.emptyList(); |
| | | if (configuredObstacles == null) { |
| | | configuredObstacles = Collections.emptyList(); |
| | | } |
| | | renderer.setCurrentBoundary(boundary, landNumber, landNumber == null ? null : landName); |
| | | renderer.setCurrentBoundary(boundary, sanitizedLandNumber, sanitizedLandNumber == null ? null : landName); |
| | | renderer.setCurrentPlannedPath(plannedPath); |
| | | renderer.setCurrentObstacles(configuredObstacles, landNumber); |
| | | boolean showBoundaryPoints = landNumber != null && boundaryPointVisibility.getOrDefault(landNumber, false); |
| | | renderer.setCurrentObstacles(configuredObstacles, sanitizedLandNumber); |
| | | boolean showBoundaryPoints = sanitizedLandNumber != null && boundaryPointVisibility.getOrDefault(sanitizedLandNumber, false); |
| | | renderer.setBoundaryPointsVisible(showBoundaryPoints); |
| | | renderer.setBoundaryPointSizeScale(showBoundaryPoints ? 0.5d : 1.0d); |
| | | } |
| | | shouye.refreshMowingIndicators(); |
| | | } |
| | | |
| | | if (changed) { |
| | | persistCurrentWorkLand(sanitizedLandNumber); |
| | | } |
| | | } |
| | | |
| | | public static String getCurrentWorkLandNumber() { |
| | | return currentWorkLandNumber; |
| | | } |
| | | |
| | | public static String getPersistedWorkLandNumber() { |
| | | return readPersistedWorkLandNumber(); |
| | | } |
| | | |
| | | private static String sanitizeLandNumber(String landNumber) { |
| | | if (landNumber == null) { |
| | | return null; |
| | | } |
| | | String trimmed = landNumber.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private static void persistCurrentWorkLand(String landNumber) { |
| | | synchronized (Dikuaiguanli.class) { |
| | | Properties props = new Properties(); |
| | | try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) { |
| | | props.load(in); |
| | | } catch (IOException ignored) { |
| | | // Use empty defaults when the configuration file is missing. |
| | | } |
| | | |
| | | if (landNumber == null) { |
| | | props.setProperty(WORK_LAND_KEY, "-1"); |
| | | } else { |
| | | props.setProperty(WORK_LAND_KEY, landNumber); |
| | | } |
| | | |
| | | try (FileOutputStream out = new FileOutputStream(PROPERTIES_FILE)) { |
| | | props.store(out, "Current work land selection updated"); |
| | | } catch (IOException ex) { |
| | | System.err.println("æ æ³ä¿åå½åä½ä¸å°å: " + ex.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static String readPersistedWorkLandNumber() { |
| | | Properties props = new Properties(); |
| | | try (FileInputStream in = new FileInputStream(PROPERTIES_FILE)) { |
| | | props.load(in); |
| | | String value = props.getProperty(WORK_LAND_KEY); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } catch (IOException ex) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private ImageIcon loadIcon(String path, int width, int height) { |
| | | try { |
| | | ImageIcon rawIcon = new ImageIcon(path); |
| | |
| | | import set.Setsys; |
| | | import ui.UIConfig; |
| | | import zhuye.Coordinate; |
| | | import zhuye.MapRenderer; |
| | | import zhuye.Shouye; |
| | | import zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | |
| | | |
| | | activeDrawingShape = shape.toLowerCase(Locale.ROOT); |
| | | |
| | | Shouye shouyeInstance = Shouye.getInstance(); |
| | | if (shouyeInstance != null) { |
| | | shouyeInstance.setHandheldMowerIconActive("handheld".equalsIgnoreCase(method)); |
| | | MapRenderer renderer = shouyeInstance.getMapRenderer(); |
| | | if (renderer != null) { |
| | | renderer.clearIdleTrail(); |
| | | } |
| | | } |
| | | |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.setActiveDeviceIdFilter(deviceId); |
| | | Coordinate.setStartSaveGngga(true); |
| | |
| | | private static final double ICON_SCALE_FACTOR = 0.8; |
| | | private static final double MIN_SCALE = 1e-6; |
| | | private static final Color FALLBACK_FILL = new Color(0, 150, 0); |
| | | private static final String DEFAULT_ICON_PATH = "image/gecaoji.png"; |
| | | private static final String HANDHELD_ICON_PATH = "image/URT.png"; |
| | | |
| | | private final Image mowerIcon; |
| | | private final Image defaultIcon; |
| | | private final Image handheldIcon; |
| | | private Image activeIcon; |
| | | private boolean handheldIconActive; |
| | | private final Ellipse2D.Double fallbackShape = new Ellipse2D.Double(); |
| | | private Point2D.Double position = new Point2D.Double(); |
| | | private boolean positionValid; |
| | | private double headingDegrees; |
| | | |
| | | public Gecaoji() { |
| | | mowerIcon = loadIcon(); |
| | | defaultIcon = loadIcon(DEFAULT_ICON_PATH); |
| | | handheldIcon = loadIcon(HANDHELD_ICON_PATH); |
| | | activeIcon = defaultIcon != null ? defaultIcon : handheldIcon; |
| | | handheldIconActive = false; |
| | | refreshFromDevice(); |
| | | } |
| | | |
| | | private Image loadIcon() { |
| | | private Image loadIcon(String path) { |
| | | try { |
| | | ImageIcon icon = new ImageIcon("image/gecaoji.png"); |
| | | ImageIcon icon = new ImageIcon(path); |
| | | if (icon.getIconWidth() <= 0 || icon.getIconHeight() <= 0) { |
| | | return null; |
| | | } |
| | | return icon.getImage(); |
| | | } catch (Exception ex) { |
| | | System.err.println("Unable to load mower icon: " + ex.getMessage()); |
| | | System.err.println("Unable to load mower icon from " + path + ": " + ex.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | |
| | | double y = parseCoordinate(device.getRealtimeY()); |
| | | double heading = parseHeading(device.getHeading()); |
| | | if (Double.isNaN(x) || Double.isNaN(y)) { |
| | | positionValid = false; |
| | | // Keep showing the last known mower position when temporary sensor glitches occur. |
| | | return; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | double worldSize = (ICON_PIXEL_SIZE * ICON_SCALE_FACTOR) / Math.max(scale, MIN_SCALE); |
| | | if (mowerIcon != null && mowerIcon.getWidth(null) > 0 && mowerIcon.getHeight(null) > 0) { |
| | | drawIcon(g2d, worldSize); |
| | | Image icon = activeIcon; |
| | | if (icon != null && icon.getWidth(null) > 0 && icon.getHeight(null) > 0) { |
| | | drawIcon(g2d, worldSize, icon); |
| | | } else { |
| | | drawFallback(g2d, worldSize); |
| | | } |
| | | } |
| | | |
| | | private void drawIcon(Graphics2D g2d, double worldSize) { |
| | | double iconWidth = mowerIcon.getWidth(null); |
| | | double iconHeight = mowerIcon.getHeight(null); |
| | | private void drawIcon(Graphics2D g2d, double worldSize, Image icon) { |
| | | double iconWidth = icon.getWidth(null); |
| | | double iconHeight = icon.getHeight(null); |
| | | double maxSide = Math.max(iconWidth, iconHeight); |
| | | double scaleFactor = worldSize / Math.max(maxSide, MIN_SCALE); |
| | | double rotationRadians = Math.toRadians(-headingDegrees); |
| | |
| | | transformed.scale(scaleFactor, scaleFactor); |
| | | transformed.translate(-iconWidth / 2.0, -iconHeight / 2.0); |
| | | g2d.setTransform(transformed); |
| | | g2d.drawImage(mowerIcon, 0, 0, null); |
| | | g2d.drawImage(icon, 0, 0, null); |
| | | g2d.setTransform(original); |
| | | } |
| | | |
| | |
| | | double worldSize = (ICON_PIXEL_SIZE * ICON_SCALE_FACTOR) / Math.max(scale, MIN_SCALE); |
| | | return worldSize / 2.0; |
| | | } |
| | | |
| | | public boolean useHandheldIcon(boolean handheldMode) { |
| | | if (handheldIconActive == handheldMode) { |
| | | return false; |
| | | } |
| | | handheldIconActive = handheldMode; |
| | | Image next = handheldMode |
| | | ? (handheldIcon != null ? handheldIcon : defaultIcon) |
| | | : defaultIcon; |
| | | if (next == null) { |
| | | next = handheldIcon; |
| | | } |
| | | activeIcon = next; |
| | | return true; |
| | | } |
| | | } |
| | |
| | | /** // ææ¡£æ³¨éå¼å§ |
| | | * Draw the planned mowing path. // ç»å¶è·¯å¾ |
| | | */ // ææ¡£æ³¨éç»æ |
| | | public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale) { // ç»å¶ä¸»æ¹æ³ |
| | | public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale) { // ç»å¶ä¸»æ¹æ³ |
| | | if (path == null || path.size() < 2) { // å¤å®ç¹æ° |
| | | return; // æ°æ®ä¸è¶³ç´æ¥è¿å |
| | | } // ifç»æ |
| | |
| | | Point2D.Double end = path.get(path.size() - 1); // ç»ç¹ |
| | | Point2D.Double prev = path.get(path.size() - 2); // åæ°ç¬¬äºä¸ªç¹ |
| | | |
| | | drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale); // ç»å¶èµ·ç¹ç®å¤´ |
| | | drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale); // ç»å¶ç»ç¹ç®å¤´ |
| | | drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // ç»å¶èµ·ç¹ç®å¤´ |
| | | drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // ç»å¶ç»ç¹ç®å¤´ |
| | | |
| | | g2d.setStroke(previous); // æ¢å¤åæè¾¹ |
| | | } // æ¹æ³ç»æ |
| | | |
| | | private static void drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale) { // ç»å¶ç®å¤´è¾
å© |
| | | private static void drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale, double sizeScale) { // ç»å¶ç®å¤´è¾
å© |
| | | if (from == null || to == null) { // å¤ç©º |
| | | return; // æ°æ®ä¸è¶³è¿å |
| | | } // ifç»æ |
| | |
| | | } // ifç»æ |
| | | |
| | | double arrowLength = Math.max(2.5, 5.5 / Math.max(0.5, scale)); // 计ç®ç®å¤´é¿åº¦ |
| | | arrowLength *= 0.5; // 缩å°ç®å¤´å°ºå¯¸ä¸ºåæ¥çä¸å |
| | | double clampedScale = sizeScale > 0 ? sizeScale : 1.0; // 鲿¢éæ³ç¼©æ¾ |
| | | arrowLength *= 0.25 * clampedScale; // 缩å°ç®å¤´è³åæ¥çä¸å |
| | | double arrowWidth = arrowLength * 0.45; // 计ç®ç®å¤´å®½åº¦ |
| | | |
| | | double ux = dx / length; // åä½åéX |
| | |
| | | import denglu.UserChuShiHua; |
| | | import gecaoji.Device; |
| | | import chuankou.SerialPortAutoConnector; |
| | | import chuankou.SerialPortNativeLoader; |
| | | import set.Setsys; |
| | | import udpdell.UDPServer; |
| | | import denglu.Denglu; |
| | | import java.awt.EventQueue; |
| | | import javax.swing.JOptionPane; |
| | | |
| | | public class Homein { |
| | |
| | | })); |
| | | |
| | | try { |
| | | // åå§åæ°æ® |
| | | UserChuShiHua.initialize(); |
| | | SerialPortNativeLoader.ensureLibraryPresent(); |
| | | // åå§åæ°æ® |
| | | UserChuShiHua.initialize(); |
| | | |
| | | Setsys setsys = new Setsys(); |
| | | setsys.initializeFromProperties(); |
| | | Device.initializeActiveDevice(setsys.getMowerId()); |
| | | Setsys setsys = new Setsys(); |
| | | setsys.initializeFromProperties(); |
| | | Device.initializeActiveDevice(setsys.getMowerId()); |
| | | |
| | | UDPServer.startAsync();//å¯å¨æ°æ®æ¥æ¶çº¿ç¨ |
| | | SerialPortAutoConnector.initialize();//å¯å¨ä¸²å£èªå¨è¿æ¥ |
| | | // SerialPortAutoConnector.initialize();//å¯å¨ä¸²å£èªå¨è¿æ¥ |
| | | |
| | | // æ¾ç¤ºåå§æ°æ® |
| | | System.out.println("åå§ç¨æ·å: " + UserChuShiHua.getProperty("userName")); |
| | |
| | | |
| | | private static void startLoginInterface() { |
| | | // å¨EDTä¸å¯å¨ç»å½çé¢ |
| | | javax.swing.SwingUtilities.invokeLater(new Runnable() { |
| | | public void run() { |
| | | try { |
| | | new Denglu().setVisible(true); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | JOptionPane.showMessageDialog(null, |
| | | "ç»å½çé¢å¯å¨å¤±è´¥: " + e.getMessage(), |
| | | "é误", |
| | | JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | EventQueue.invokeLater(() -> { |
| | | try { |
| | | new Denglu().setVisible(true); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | JOptionPane.showMessageDialog(null, |
| | | "ç»å½çé¢å¯å¨å¤±è´¥: " + e.getMessage(), |
| | | "é误", |
| | | JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | }); |
| | | } |
| | |
| | | } |
| | | |
| | | List<PathSegment> generate() { |
| | | if (!"spiral".equals(mode)) { |
| | | return generateParallelPath(); |
| | | if ("spiral".equals(mode)) { |
| | | return generateSpiralPath(); |
| | | } |
| | | return generateParallelPath(); |
| | | } |
| | |
| | | return path; |
| | | } |
| | | |
| | | private List<PathSegment> generateSpiralPath() { |
| | | Geometry safeArea = buildSafeArea(); |
| | | if (safeArea == null || safeArea.isEmpty()) { |
| | | System.err.println("å®å
¨åºåä¸ºç©ºï¼æ æ³çæèºæè·¯å¾"); |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | List<PathSegment> spiral = luoxuan.generateSpiralPath(safeArea, width); |
| | | if (spiral.isEmpty()) { |
| | | return spiral; |
| | | } |
| | | |
| | | postProcess(spiral); |
| | | PathSegment firstMowing = null; |
| | | PathSegment endCandidate = null; |
| | | for (int i = 0; i < spiral.size(); i++) { |
| | | PathSegment seg = spiral.get(i); |
| | | if (seg != null && seg.isMowing) { |
| | | if (firstMowing == null) { |
| | | firstMowing = seg; |
| | | } |
| | | endCandidate = seg; |
| | | } |
| | | } |
| | | if (firstMowing != null) { |
| | | firstMowing.setAsStartPoint(); |
| | | } |
| | | if (endCandidate != null && endCandidate != firstMowing) { |
| | | endCandidate.setAsEndPoint(); |
| | | } |
| | | return spiral; |
| | | } |
| | | |
| | | private Geometry buildSafeArea() { |
| | | try { |
| | | Coordinate[] coords = polygon.toArray(new Coordinate[0]); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package lujing; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | import org.locationtech.jts.geom.Coordinate; |
| | | import org.locationtech.jts.geom.Geometry; |
| | | import org.locationtech.jts.geom.LinearRing; |
| | | import org.locationtech.jts.geom.Polygon; |
| | | import org.locationtech.jts.geom.TopologyException; |
| | | import org.locationtech.jts.operation.buffer.BufferParameters; |
| | | |
| | | import lujing.Lunjingguihua.PathSegment; |
| | | |
| | | /** |
| | | * Utility class that produces spiral mowing paths by iteratively offsetting a safe area polygon. |
| | | * ä¼åçï¼æ¹è¿è·¯å¾çæé»è¾ï¼åå°ç©ºé©¶è·ç¦»ï¼ä¼åè·¯å¾è¿ç»æ§ |
| | | */ |
| | | public final class luoxuan { |
| | | |
| | | private static final int MAX_ITERATIONS = 512; |
| | | private static final double AREA_EPSILON = 1e-2; |
| | | private static final double LENGTH_EPSILON = 1e-6; |
| | | private static final double MIN_BUFFER_RATIO = 0.6; // æå°ç¼å²æ¯ä¾ |
| | | |
| | | private luoxuan() { |
| | | } |
| | | |
| | | /** |
| | | * Generate optimized spiral mowing paths with improved continuity |
| | | */ |
| | | public static List<PathSegment> generateOptimizedSpiralPath(Geometry safeArea, double laneWidth) { |
| | | if (safeArea == null || safeArea.isEmpty() || !Double.isFinite(laneWidth) || laneWidth <= 0) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | // 1. æ¸
çå ä½ä½ï¼ç¡®ä¿æææ§ |
| | | Geometry working = cleanGeometry(safeArea); |
| | | if (working.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | // 2. æå主å¤è¾¹å½¢ï¼éæ©é¢ç§¯æå¤§çï¼ |
| | | Polygon mainPolygon = extractMainPolygon(working); |
| | | if (mainPolygon == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | // 3. 计ç®èºæè·¯å¾ |
| | | List<PathSegment> segments = new ArrayList<>(); |
| | | Coordinate cursor = clone(findOptimalStartPoint(mainPolygon)); |
| | | Geometry currentLayer = mainPolygon; // start from the outer boundaryand peel inwards |
| | | |
| | | for (int iteration = 0; iteration < MAX_ITERATIONS; iteration++) { |
| | | Geometry layerGeometry = cleanGeometry(currentLayer); |
| | | if (layerGeometry == null || layerGeometry.isEmpty()) { |
| | | break; |
| | | } |
| | | |
| | | List<Polygon> polygons = extractPolygons(layerGeometry); |
| | | if (polygons.isEmpty()) { |
| | | break; |
| | | } |
| | | polygons.sort(Comparator.comparingDouble(Polygon::getArea).reversed()); |
| | | |
| | | try { |
| | | for (Polygon polygon : polygons) { |
| | | LinearRing outer = polygon.getExteriorRing(); |
| | | if (outer == null || outer.getNumPoints() < 4) { |
| | | continue; |
| | | } |
| | | |
| | | cursor = processRing(outer.getCoordinates(), true, cursor, segments); |
| | | |
| | | for (int holeIndex = 0; holeIndex < polygon.getNumInteriorRing(); holeIndex++) { |
| | | LinearRing hole = polygon.getInteriorRingN(holeIndex); |
| | | if (hole == null || hole.getNumPoints() < 4) { |
| | | continue; |
| | | } |
| | | cursor = processRing(hole.getCoordinates(), false, cursor, segments); |
| | | } |
| | | } |
| | | } catch (TopologyException ex) { |
| | | break; |
| | | } |
| | | |
| | | if (!canShrinkFurther(polygons, laneWidth)) { |
| | | break; |
| | | } |
| | | |
| | | Geometry nextLayer; |
| | | try { |
| | | nextLayer = layerGeometry.buffer( |
| | | -laneWidth, |
| | | BufferParameters.DEFAULT_QUADRANT_SEGMENTS, |
| | | BufferParameters.CAP_FLAT |
| | | ); |
| | | } catch (TopologyException ex) { |
| | | break; |
| | | } |
| | | |
| | | if (nextLayer.isEmpty() || nextLayer.getArea() < AREA_EPSILON) { |
| | | break; |
| | | } |
| | | |
| | | double areaDelta = Math.abs(layerGeometry.getArea() - nextLayer.getArea()); |
| | | if (areaDelta <= AREA_EPSILON) { |
| | | break; |
| | | } |
| | | |
| | | currentLayer = nextLayer; |
| | | } |
| | | |
| | | // 4. ä¼åè·¯å¾è¿æ¥ |
| | | optimizePathConnections(segments); |
| | | |
| | | // 5. æ è®°ç«¯ç¹ |
| | | markEndpoints(segments); |
| | | |
| | | return segments; |
| | | } |
| | | |
| | | /** |
| | | * Backward compatible entry that delegates to the optimized implementation. |
| | | */ |
| | | public static List<PathSegment> generateSpiralPath(Geometry safeArea, double laneWidth) { |
| | | return generateOptimizedSpiralPath(safeArea, laneWidth); |
| | | } |
| | | |
| | | /** |
| | | * æ¸
çå ä½ä½ï¼ç¡®ä¿æææ§ |
| | | */ |
| | | private static Geometry cleanGeometry(Geometry geometry) { |
| | | if (geometry == null) return null; |
| | | try { |
| | | return geometry.buffer(0.0); |
| | | } catch (Exception e) { |
| | | return geometry; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æå主å¤è¾¹å½¢ï¼é¢ç§¯æå¤§çï¼ |
| | | */ |
| | | private static Polygon extractMainPolygon(Geometry geometry) { |
| | | List<Polygon> polygons = extractPolygons(geometry); |
| | | if (polygons.isEmpty()) return null; |
| | | |
| | | // æé¢ç§¯æåºï¼éæ©æå¤§ç |
| | | polygons.sort((p1, p2) -> Double.compare(p2.getArea(), p1.getArea())); |
| | | return polygons.get(0); |
| | | } |
| | | |
| | | /** |
| | | * å¯»æ¾æä¼èµ·ç¹ï¼ç¦»å¤è¾¹å½¢ä¸å¿æè¿çç¹ï¼ |
| | | */ |
| | | private static Coordinate findOptimalStartPoint(Polygon polygon) { |
| | | if (polygon == null) return null; |
| | | |
| | | Coordinate center = polygon.getCentroid().getCoordinate(); |
| | | LinearRing ring = polygon.getExteriorRing(); |
| | | Coordinate[] coords = ring.getCoordinates(); |
| | | |
| | | Coordinate nearest = coords[0]; |
| | | double minDist = Double.MAX_VALUE; |
| | | |
| | | for (Coordinate coord : coords) { |
| | | double dist = coord.distance(center); |
| | | if (dist < minDist) { |
| | | minDist = dist; |
| | | nearest = coord; |
| | | } |
| | | } |
| | | |
| | | return nearest; |
| | | } |
| | | |
| | | /** |
| | | * ä¼åè·¯å¾è¿æ¥ï¼åå°ç©ºé©¶è·ç¦» |
| | | */ |
| | | private static void optimizePathConnections(List<PathSegment> segments) { |
| | | if (segments == null || segments.size() < 2) { |
| | | return; |
| | | } |
| | | |
| | | List<PathSegment> optimized = new ArrayList<>(segments.size()); |
| | | PathSegment previous = null; |
| | | |
| | | for (PathSegment segment : segments) { |
| | | if (segment == null || segment.start == null || segment.end == null) { |
| | | continue; |
| | | } |
| | | if (isDegenerate(segment)) { |
| | | continue; |
| | | } |
| | | |
| | | if (previous != null |
| | | && previous.isMowing == segment.isMowing |
| | | && equals2D(previous.start, segment.start) |
| | | && equals2D(previous.end, segment.end)) { |
| | | continue; // è·³è¿é夿®µ |
| | | } |
| | | |
| | | optimized.add(segment); |
| | | previous = segment; |
| | | } |
| | | |
| | | segments.clear(); |
| | | segments.addAll(optimized); |
| | | } |
| | | |
| | | /** |
| | | * æ£æ¥æ¯å¦å¯ä»¥ç»§ç»ç¼å² |
| | | */ |
| | | private static boolean canShrinkFurther(List<Polygon> polygons, double bufferDistance) { |
| | | if (polygons == null || polygons.isEmpty()) { |
| | | return false; |
| | | } |
| | | |
| | | for (Polygon polygon : polygons) { |
| | | if (polygon == null || polygon.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | double width = polygon.getEnvelopeInternal().getWidth(); |
| | | double height = polygon.getEnvelopeInternal().getHeight(); |
| | | double minDimension = Math.min(width, height); |
| | | |
| | | if (minDimension <= bufferDistance * 2 * MIN_BUFFER_RATIO) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * æ è®°èµ·ç¹åç»ç¹ |
| | | */ |
| | | private static void markEndpoints(List<PathSegment> segments) { |
| | | if (segments == null || segments.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | // 寻æ¾ç¬¬ä¸ä¸ªå²è段ä½ä¸ºèµ·ç¹ |
| | | PathSegment firstMowing = null; |
| | | for (PathSegment seg : segments) { |
| | | if (seg != null && seg.isMowing) { |
| | | firstMowing = seg; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | // å¯»æ¾æåä¸ä¸ªå²è段ä½ä¸ºç»ç¹ |
| | | PathSegment lastMowing = null; |
| | | for (int i = segments.size() - 1; i >= 0; i--) { |
| | | PathSegment seg = segments.get(i); |
| | | if (seg != null && seg.isMowing) { |
| | | lastMowing = seg; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (firstMowing != null) { |
| | | firstMowing.setAsStartPoint(); |
| | | } |
| | | if (lastMowing != null && lastMowing != firstMowing) { |
| | | lastMowing.setAsEndPoint(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ£æ¥çº¿æ®µæ¯å¦éåï¼é¿åº¦è¿å°ï¼ |
| | | */ |
| | | private static boolean isDegenerate(PathSegment segment) { |
| | | if (segment == null || segment.start == null || segment.end == null) { |
| | | return true; |
| | | } |
| | | double dx = segment.start.x - segment.end.x; |
| | | double dy = segment.start.y - segment.end.y; |
| | | return Math.hypot(dx, dy) <= LENGTH_EPSILON; |
| | | } |
| | | |
| | | /** |
| | | * æåå¤è¾¹å½¢ï¼ä¸åæ¹æ³ç¸åï¼ |
| | | */ |
| | | private static List<Polygon> extractPolygons(Geometry geometry) { |
| | | if (geometry == null || geometry.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | List<Polygon> result = new ArrayList<>(); |
| | | |
| | | if (geometry instanceof Polygon) { |
| | | result.add((Polygon) geometry); |
| | | } else if (geometry instanceof org.locationtech.jts.geom.MultiPolygon) { |
| | | org.locationtech.jts.geom.MultiPolygon mp = (org.locationtech.jts.geom.MultiPolygon) geometry; |
| | | for (int i = 0; i < mp.getNumGeometries(); i++) { |
| | | Geometry g = mp.getGeometryN(i); |
| | | if (g instanceof Polygon) { |
| | | result.add((Polygon) g); |
| | | } |
| | | } |
| | | } else if (geometry instanceof org.locationtech.jts.geom.GeometryCollection) { |
| | | org.locationtech.jts.geom.GeometryCollection gc = (org.locationtech.jts.geom.GeometryCollection) geometry; |
| | | for (int i = 0; i < gc.getNumGeometries(); i++) { |
| | | Geometry child = gc.getGeometryN(i); |
| | | result.addAll(extractPolygons(child)); |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * å¤å¶åæ |
| | | */ |
| | | private static Coordinate clone(Coordinate source) { |
| | | return source == null ? null : new Coordinate(source.x, source.y); |
| | | } |
| | | |
| | | /** |
| | | * æ¯è¾ä¸¤ä¸ªåæ æ¯å¦ç¸åï¼2Dï¼ |
| | | */ |
| | | private static boolean equals2D(Coordinate a, Coordinate b) { |
| | | if (a == b) return true; |
| | | if (a == null || b == null) return false; |
| | | return a.distance(b) <= LENGTH_EPSILON; |
| | | } |
| | | |
| | | private static Coordinate processRing(Coordinate[] coords, |
| | | boolean forward, |
| | | Coordinate cursor, |
| | | List<PathSegment> segments) { |
| | | if (coords == null || coords.length < 4) { |
| | | return cursor; |
| | | } |
| | | |
| | | List<Coordinate> base = new ArrayList<>(coords.length - 1); |
| | | for (int i = 0; i < coords.length - 1; i++) { |
| | | Coordinate cloned = clone(coords[i]); |
| | | if (cloned != null) { |
| | | base.add(cloned); |
| | | } |
| | | } |
| | | |
| | | if (base.size() < 2) { |
| | | return cursor; |
| | | } |
| | | |
| | | if (!forward) { |
| | | Collections.reverse(base); |
| | | } |
| | | |
| | | int startIndex = 0; |
| | | if (cursor != null) { |
| | | startIndex = findNearestIndex(base, cursor); |
| | | } |
| | | |
| | | List<Coordinate> ordered = new ArrayList<>(base.size()); |
| | | for (int i = 0; i < base.size(); i++) { |
| | | int index = (startIndex + i) % base.size(); |
| | | ordered.add(clone(base.get(index))); |
| | | } |
| | | |
| | | Coordinate firstCoord = ordered.get(0); |
| | | if (cursor != null && !equals2D(cursor, firstCoord)) { |
| | | PathSegment transfer = new PathSegment(clone(cursor), clone(firstCoord), false); |
| | | if (!isDegenerate(transfer)) { |
| | | segments.add(transfer); |
| | | } |
| | | } |
| | | |
| | | for (int i = 0; i < ordered.size(); i++) { |
| | | Coordinate start = ordered.get(i); |
| | | Coordinate end = ordered.get((i + 1) % ordered.size()); |
| | | if (equals2D(start, end)) { |
| | | continue; |
| | | } |
| | | PathSegment mowing = new PathSegment(clone(start), clone(end), true); |
| | | segments.add(mowing); |
| | | } |
| | | |
| | | return clone(firstCoord); |
| | | } |
| | | |
| | | private static int findNearestIndex(List<Coordinate> coordinates, Coordinate reference) { |
| | | if (coordinates == null || coordinates.isEmpty() || reference == null) { |
| | | return 0; |
| | | } |
| | | double bestDistance = Double.MAX_VALUE; |
| | | int bestIndex = 0; |
| | | for (int i = 0; i < coordinates.size(); i++) { |
| | | Coordinate candidate = coordinates.get(i); |
| | | if (candidate == null) { |
| | | continue; |
| | | } |
| | | double distance = reference.distance(candidate); |
| | | if (distance < bestDistance) { |
| | | bestDistance = distance; |
| | | bestIndex = i; |
| | | } |
| | | } |
| | | return bestIndex; |
| | | } |
| | | } |
| | |
| | | |
| | | // 设置项ç»ä»¶ |
| | | private JLabel mowerIdLabel; |
| | | private JLabel baseStationIdLabel; |
| | | private JLabel handheldMarkerLabel; |
| | | private JLabel simCardNumberLabel; |
| | | private JLabel baseStationSimLabel; |
| | |
| | | private JLabel idleTrailDurationLabel; |
| | | |
| | | private JButton mowerIdEditBtn; |
| | | private JButton baseStationIdEditBtn; |
| | | private JButton handheldEditBtn; |
| | | private JButton checkUpdateBtn; |
| | | private JButton systemDebugButton; |
| | |
| | | setData.getMowerId() != null ? setData.getMowerId() : "æªè®¾ç½®", true); |
| | | mowerIdLabel = (JLabel) mowerIdPanel.getClientProperty("valueLabel"); |
| | | mowerIdEditBtn = (JButton) mowerIdPanel.getClientProperty("editButton"); |
| | | |
| | | JPanel baseStationIdPanel = createSettingItemPanel("å·®ååºåç«ç¼å·", |
| | | resolveBaseStationId(), true); |
| | | baseStationIdLabel = (JLabel) baseStationIdPanel.getClientProperty("valueLabel"); |
| | | baseStationIdEditBtn = (JButton) baseStationIdPanel.getClientProperty("editButton"); |
| | | |
| | | JPanel handheldPanel = createSettingItemPanel("ä¾¿æºæç¹å¨ç¼å·", |
| | | setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "æªè®¾ç½®", true); |
| | |
| | | // APPçæ¬ |
| | | JPanel appVersionPanel = createAppVersionPanel(); |
| | | |
| | | addRowWithSpacing(panel, mowerIdPanel); |
| | | addRowWithSpacing(panel, mowerIdPanel); |
| | | addRowWithSpacing(panel, baseStationIdPanel); |
| | | addRowWithSpacing(panel, handheldPanel); |
| | | addRowWithSpacing(panel, simCardPanel); |
| | | addRowWithSpacing(panel, baseStationSimPanel); |
| | |
| | | mowerIdLabel.setText(setData.getMowerId() != null ? setData.getMowerId() : "æªè®¾ç½®"); |
| | | } |
| | | |
| | | if (baseStationIdLabel != null) { |
| | | baseStationIdLabel.setText(resolveBaseStationId()); |
| | | } |
| | | |
| | | if (handheldMarkerLabel != null) { |
| | | handheldMarkerLabel.setText(setData.getHandheldMarkerId() != null ? setData.getHandheldMarkerId() : "æªè®¾ç½®"); |
| | | } |
| | |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private String resolveBaseStationId() { |
| | | if (baseStation == null) { |
| | | return "æªè®¾ç½®"; |
| | | } |
| | | String value = baseStation.getDeviceId(); |
| | | if (value == null) { |
| | | return "æªè®¾ç½®"; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return "æªè®¾ç½®"; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private void setupEventHandlers() { |
| | | // å²èæºç¼å·ç¼è¾æé®äºä»¶ |
| | | if (mowerIdEditBtn != null) { |
| | | mowerIdEditBtn.addActionListener(e -> editMowerId()); |
| | | } |
| | | |
| | | if (baseStationIdEditBtn != null) { |
| | | baseStationIdEditBtn.addActionListener(e -> editBaseStationId()); |
| | | } |
| | | |
| | | // æ£æ¥æ´æ°æé®äºä»¶ |
| | | if (checkUpdateBtn != null) { |
| | |
| | | } |
| | | } |
| | | |
| | | private void editBaseStationId() { |
| | | String currentValue = "æªè®¾ç½®".equals(resolveBaseStationId()) ? "" : resolveBaseStationId(); |
| | | String newValue = (String) JOptionPane.showInputDialog(this, |
| | | "请è¾å
¥å·®ååºåç«ç¼å·:", |
| | | "ä¿®æ¹å·®ååºåç«ç¼å·", |
| | | JOptionPane.QUESTION_MESSAGE, |
| | | null, |
| | | null, |
| | | currentValue); |
| | | |
| | | if (newValue == null) { |
| | | return; |
| | | } |
| | | |
| | | newValue = newValue.trim(); |
| | | if (newValue.isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, "å·®ååºåç«ç¼å·ä¸è½ä¸ºç©º", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | baseStation.updateByDeviceId(newValue, |
| | | baseStation.getInstallationCoordinates(), |
| | | baseStation.getIotSimCardNumber(), |
| | | baseStation.getDeviceActivationTime(), |
| | | baseStation.getDataUpdateTime()); |
| | | baseStation.load(); |
| | | if (baseStationIdLabel != null) { |
| | | baseStationIdLabel.setText(resolveBaseStationId()); |
| | | } |
| | | JOptionPane.showMessageDialog(this, "å·®ååºåç«ç¼å·æ´æ°æå", "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | } catch (IllegalArgumentException ex) { |
| | | JOptionPane.showMessageDialog(this, ex.getMessage(), "è¾å
¥é误", JOptionPane.WARNING_MESSAGE); |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | JOptionPane.showMessageDialog(this, "å·®ååºåç«ç¼å·æ´æ°å¤±è´¥", "é误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } |
| | | |
| | | private void editIdleTrailDuration() { |
| | | int currentSeconds = setData != null ? setData.getIdleTrailDurationSeconds() : Setsys.DEFAULT_IDLE_TRAIL_DURATION_SECONDS; |
| | | if (currentSeconds <= 0) { |
| | |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.Comparator; |
| | | import java.awt.geom.Point2D; |
| | | |
| | | import baseStation.BaseStation; |
| | | import bianjie.jisuanmianjie; |
| | | import dikuai.Dikuai; |
| | | import dikuai.Dikuaiguanli; |
| | | import gecaoji.Device; |
| | | import bianjie.bianjieguihua2; |
| | | import lujing.Lunjingguihua; |
| | | import ui.UIConfig; |
| | |
| | | private final Color LIGHT_TEXT = new Color(108, 117, 125); |
| | | private final Color BORDER_COLOR = new Color(222, 226, 230); |
| | | private final Color SUCCESS_COLOR = new Color(40, 167, 69); |
| | | private final Color ERROR_COLOR = new Color(220, 53, 69); |
| | | private static final String KEY_PATH_MESSAGE_TEXT = "__pathMessageText"; |
| | | private static final String KEY_PATH_MESSAGE_SUCCESS = "__pathMessageSuccess"; |
| | | |
| | | // æ¥éª¤é¢æ¿ |
| | | private JPanel mainPanel; |
| | |
| | | private JButton prevButton; |
| | | private JButton nextButton; |
| | | private JButton createButton; |
| | | private JButton previewButton; |
| | | private Component previewButtonSpacer; |
| | | private JLabel boundaryCountLabel; |
| | | private JPanel obstacleListContainer; |
| | | private JTextArea pathGenerationMessageArea; |
| | | private JPanel pathMessageWrapper; |
| | | |
| | | // å°åæ°æ® |
| | | private Map<String, String> dikuaiData = new HashMap<>(); |
| | |
| | | |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | stepPanel.add(generatePathButton); |
| | | stepPanel.add(Box.createRigidArea(new Dimension(0, 12))); |
| | | |
| | | pathMessageWrapper = new JPanel(new BorderLayout()); |
| | | pathMessageWrapper.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | pathMessageWrapper.setBackground(PRIMARY_LIGHT); |
| | | pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(PRIMARY_COLOR, 1), |
| | | BorderFactory.createEmptyBorder(12, 12, 12, 12) |
| | | )); |
| | | pathMessageWrapper.setVisible(false); |
| | | |
| | | pathGenerationMessageArea = new JTextArea(); |
| | | pathGenerationMessageArea.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | pathGenerationMessageArea.setForeground(TEXT_COLOR); |
| | | pathGenerationMessageArea.setOpaque(false); |
| | | pathGenerationMessageArea.setEditable(false); |
| | | pathGenerationMessageArea.setLineWrap(true); |
| | | pathGenerationMessageArea.setWrapStyleWord(true); |
| | | pathGenerationMessageArea.setFocusable(false); |
| | | pathGenerationMessageArea.setBorder(null); |
| | | |
| | | pathMessageWrapper.add(pathGenerationMessageArea, BorderLayout.CENTER); |
| | | stepPanel.add(pathMessageWrapper); |
| | | stepPanel.add(Box.createVerticalGlue()); |
| | | |
| | | return stepPanel; |
| | |
| | | private void generateMowingPath() { |
| | | if (!dikuaiData.containsKey("boundaryDrawn")) { |
| | | JOptionPane.showMessageDialog(this, "请å
å®æè¾¹çç»å¶ååçæè·¯å¾", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("请å
å®æè¾¹çç»å¶ååçæè·¯å¾ã", false); |
| | | setPathAvailability(false); |
| | | showStep(2); |
| | | return; |
| | | } |
| | |
| | | } |
| | | if (boundaryCoords == null) { |
| | | JOptionPane.showMessageDialog(this, "æªæ¾å°ææçå°åè¾¹çåæ ï¼æ æ³çæè·¯å¾", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | } |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("æªæ¾å°ææçå°åè¾¹çåæ ï¼æ æ³çæè·¯å¾ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | |
| | |
| | | Object widthObj = mowingWidthSpinner.getValue(); |
| | | if (!(widthObj instanceof Number)) { |
| | | JOptionPane.showMessageDialog(this, "å²è宽度è¾å
¥æ æ", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | } |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("å²è宽度è¾å
¥æ æï¼è¯·éæ°è¾å
¥ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | double widthCm = ((Number) widthObj).doubleValue(); |
| | | if (widthCm <= 0) { |
| | | JOptionPane.showMessageDialog(this, "å²è宽度å¿
须大äº0", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | } |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("å²è宽度å¿
须大äº0ï¼è¯·éæ°è®¾ç½®ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | dikuaiData.put("mowingWidth", widthObj.toString()); |
| | |
| | | String plannedPath = Lunjingguihua.formatPathSegments(segments); |
| | | if (!isMeaningfulValue(plannedPath)) { |
| | | JOptionPane.showMessageDialog(this, "çæå²èè·¯å¾å¤±è´¥: çæç»æä¸ºç©º", "é误", JOptionPane.ERROR_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | } |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("çæå²èè·¯å¾å¤±è´¥ï¼çæç»æä¸ºç©ºã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | dikuaiData.put("plannedPath", plannedPath); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(true); |
| | | if (isMeaningfulValue(boundaryCoords)) { |
| | | dikuaiData.put("boundaryCoordinates", boundaryCoords); |
| | | } |
| | | JOptionPane.showMessageDialog(this, |
| | | "å·²æ ¹æ®å½å设置çæå²èè·¯å¾ï¼å
±çæ " + segments.size() + " 段ã", |
| | | "æå", |
| | | JOptionPane.INFORMATION_MESSAGE); |
| | | if (isMeaningfulValue(obstacleCoords)) { |
| | | dikuaiData.put("obstacleCoordinates", obstacleCoords); |
| | | } |
| | | dikuaiData.put("plannedPath", plannedPath); |
| | | setPathAvailability(true); |
| | | showPathGenerationMessage( |
| | | "å·²æ ¹æ®å½å设置çæå²èè·¯å¾ï¼å
±çæ " + segments.size() + " 段ã\nç¹å»âé¢è§âæé®å¯å¨ä¸»é¡µé¢æ¥çææã", |
| | | true); |
| | | } catch (IllegalArgumentException ex) { |
| | | JOptionPane.showMessageDialog(this, "çæå²èè·¯å¾å¤±è´¥: " + ex.getMessage(), "é误", JOptionPane.ERROR_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | } |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("çæå²èè·¯å¾å¤±è´¥ï¼" + ex.getMessage(), false); |
| | | setPathAvailability(false); |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | JOptionPane.showMessageDialog(this, "çæå²èè·¯å¾æ¶åçå¼å¸¸: " + ex.getMessage(), "é误", JOptionPane.ERROR_MESSAGE); |
| | | if (createButton != null) { |
| | | createButton.setEnabled(false); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("çæå²èè·¯å¾æ¶åçå¼å¸¸ï¼" + ex.getMessage(), false); |
| | | setPathAvailability(false); |
| | | } |
| | | } |
| | | |
| | | private void previewMowingPath() { |
| | | if (!hasGeneratedPath()) { |
| | | showPathGenerationMessage("请å
çæå²èè·¯å¾ååé¢è§ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | |
| | | persistStep3Inputs(); |
| | | |
| | | String landNumber = getPendingLandNumber(); |
| | | String trimmedAreaName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; |
| | | String displayAreaName = isMeaningfulValue(trimmedAreaName) ? trimmedAreaName : landNumber; |
| | | |
| | | String plannedPath = dikuaiData.get("plannedPath"); |
| | | if (!isMeaningfulValue(plannedPath)) { |
| | | showPathGenerationMessage("请å
çæå²èè·¯å¾ååé¢è§ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | |
| | | String boundary = null; |
| | | Dikuai pending = getOrCreatePendingDikuai(); |
| | | if (pending != null) { |
| | | boundary = normalizeCoordinateValue(pending.getBoundaryCoordinates()); |
| | | } |
| | | if (boundary == null) { |
| | | boundary = normalizeCoordinateValue(dikuaiData.get("boundaryCoordinates")); |
| | | } |
| | | |
| | | String obstacles = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates")); |
| | | if (!isMeaningfulValue(obstacles)) { |
| | | obstacles = resolveObstaclePayloadFromConfig(landNumber); |
| | | if (isMeaningfulValue(obstacles)) { |
| | | dikuaiData.put("obstacleCoordinates", obstacles); |
| | | } |
| | | } |
| | | |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye == null) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³æå¼ä¸»é¡µé¢ï¼è¯·ç¨åéè¯", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | dikuaiData.put("areaName", trimmedAreaName); |
| | | if (isMeaningfulValue(boundary)) { |
| | | dikuaiData.put("boundaryCoordinates", boundary); |
| | | } |
| | | |
| | | pendingLandNumber = landNumber; |
| | | captureSessionSnapshot(); |
| | | |
| | | resumeRequested = true; |
| | | boolean started = shouye.startMowingPathPreview( |
| | | landNumber, |
| | | displayAreaName, |
| | | boundary, |
| | | obstacles, |
| | | plannedPath, |
| | | AddDikuai::resumeFromPreview |
| | | ); |
| | | if (!started) { |
| | | resumeRequested = false; |
| | | JOptionPane.showMessageDialog(this, "æ æ³å¯å¨é¢è§ï¼è¯·ç¨ååè¯", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | closePreviewAndDispose(); |
| | | } |
| | | |
| | | private void persistStep3Inputs() { |
| | | String trimmedName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; |
| | | dikuaiData.put("areaName", trimmedName); |
| | | |
| | | if (mowingPatternCombo != null) { |
| | | Object selection = mowingPatternCombo.getSelectedItem(); |
| | | if (selection != null) { |
| | | dikuaiData.put("mowingPattern", selection.toString()); |
| | | } |
| | | } |
| | | |
| | | if (mowingWidthSpinner != null) { |
| | | Object widthValue = mowingWidthSpinner.getValue(); |
| | | if (widthValue instanceof Number) { |
| | | int widthInt = ((Number) widthValue).intValue(); |
| | | dikuaiData.put("mowingWidth", Integer.toString(widthInt)); |
| | | } else if (widthValue != null) { |
| | | dikuaiData.put("mowingWidth", widthValue.toString()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void captureSessionSnapshot() { |
| | | if (activeSession == null) { |
| | | activeSession = new DrawingSession(); |
| | | } |
| | | String landNumber = getPendingLandNumber(); |
| | | activeSession.landNumber = landNumber; |
| | | activeSession.areaName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; |
| | | activeSession.drawingCompleted = true; |
| | | activeSession.data = new HashMap<>(dikuaiData); |
| | | } |
| | | |
| | | private void closePreviewAndDispose() { |
| | | setVisible(false); |
| | | dispose(); |
| | | } |
| | | |
| | | private JButton createPrimaryButton(String text, int fontSize) { |
| | |
| | | |
| | | return button; |
| | | } |
| | | |
| | | private void showPathGenerationMessage(String message, boolean success) { |
| | | if (pathGenerationMessageArea == null || pathMessageWrapper == null) { |
| | | return; |
| | | } |
| | | String display = message == null ? "" : message.trim(); |
| | | if (display.isEmpty()) { |
| | | dikuaiData.remove(KEY_PATH_MESSAGE_TEXT); |
| | | dikuaiData.remove(KEY_PATH_MESSAGE_SUCCESS); |
| | | } else { |
| | | dikuaiData.put(KEY_PATH_MESSAGE_TEXT, display); |
| | | dikuaiData.put(KEY_PATH_MESSAGE_SUCCESS, success ? "true" : "false"); |
| | | } |
| | | pathGenerationMessageArea.setText(display); |
| | | Color borderColor = success ? PRIMARY_COLOR : ERROR_COLOR; |
| | | Color textColor = success ? PRIMARY_DARK : ERROR_COLOR; |
| | | Color backgroundColor = success ? PRIMARY_LIGHT : new Color(255, 235, 238); |
| | | pathGenerationMessageArea.setForeground(textColor); |
| | | pathMessageWrapper.setBackground(backgroundColor); |
| | | pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(borderColor, 1), |
| | | BorderFactory.createEmptyBorder(12, 12, 12, 12) |
| | | )); |
| | | pathMessageWrapper.setVisible(!display.isEmpty()); |
| | | pathMessageWrapper.revalidate(); |
| | | pathMessageWrapper.repaint(); |
| | | } |
| | | |
| | | private void setPathAvailability(boolean available) { |
| | | boolean effective = available && currentStep == 3; |
| | | if (createButton != null) { |
| | | createButton.setEnabled(effective); |
| | | } |
| | | if (previewButton != null) { |
| | | boolean visible = previewButton.isVisible(); |
| | | previewButton.setEnabled(effective && visible); |
| | | } |
| | | } |
| | | |
| | | private JPanel createButtonPanel() { |
| | | JPanel buttonPanel = new JPanel(); |
| | |
| | | nextButton = createPrimaryButton("ä¸ä¸æ¥", 16); |
| | | createButton = createPrimaryButton("ä¿å", 16); |
| | | createButton.setVisible(false); |
| | | createButton.setEnabled(false); |
| | | createButton.setEnabled(false); |
| | | |
| | | previewButton = createPrimaryButton("é¢è§", 16); |
| | | previewButton.setVisible(false); |
| | | previewButton.setEnabled(false); |
| | | |
| | | previewButtonSpacer = Box.createHorizontalStrut(15); |
| | | previewButtonSpacer.setVisible(false); |
| | | |
| | | buttonPanel.add(prevButton); |
| | | buttonPanel.add(Box.createHorizontalGlue()); |
| | | buttonPanel.add(nextButton); |
| | | buttonPanel.add(previewButtonSpacer); |
| | | buttonPanel.add(previewButton); |
| | | buttonPanel.add(Box.createHorizontalStrut(15)); |
| | | buttonPanel.add(createButton); |
| | | |
| | |
| | | return true; |
| | | } |
| | | |
| | | private static String buildOriginalBoundaryString() { |
| | | if (Coordinate.coordinates == null || Coordinate.coordinates.isEmpty()) { |
| | | private static List<Coordinate> sanitizeCoordinateList(List<Coordinate> source) { |
| | | if (source == null || source.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | List<Coordinate> snapshot = new ArrayList<>(); |
| | | for (Coordinate coordinate : source) { |
| | | if (coordinate != null) { |
| | | snapshot.add(coordinate); |
| | | } |
| | | } |
| | | if (snapshot.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | DecimalFormat latLonFormat = new DecimalFormat("0.000000"); |
| | | LinkedHashMap<String, Coordinate> unique = new LinkedHashMap<>(); |
| | | for (Coordinate coord : snapshot) { |
| | | double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection()); |
| | | double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection()); |
| | | String key = latLonFormat.format(lat) + "," + latLonFormat.format(lon); |
| | | unique.putIfAbsent(key, coord); |
| | | } |
| | | return new ArrayList<>(unique.values()); |
| | | } |
| | | |
| | | private static String buildOriginalBoundaryString(List<Coordinate> coordinates) { |
| | | if (coordinates == null || coordinates.isEmpty()) { |
| | | return "-1"; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | DecimalFormat latLonFormat = new DecimalFormat("0.000000"); |
| | | DecimalFormat elevationFormat = new DecimalFormat("0.00"); |
| | | for (Coordinate coord : Coordinate.coordinates) { |
| | | for (Coordinate coord : coordinates) { |
| | | double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection()); |
| | | double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection()); |
| | | double elevation = coord.getElevation(); |
| | |
| | | } |
| | | |
| | | private static BoundarySnapshotResult computeBoundarySnapshot() { |
| | | int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0; |
| | | if (count < 3) { |
| | | List<Coordinate> uniqueCoordinates; |
| | | synchronized (Coordinate.coordinates) { |
| | | uniqueCoordinates = sanitizeCoordinateList(Coordinate.coordinates); |
| | | Coordinate.coordinates.clear(); |
| | | Coordinate.coordinates.addAll(uniqueCoordinates); |
| | | } |
| | | |
| | | if (uniqueCoordinates.size() < 3) { |
| | | return BoundarySnapshotResult.failure("ééçè¾¹çç¹ä¸è¶³ï¼æ æ³çæå°åè¾¹ç", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | |
| | |
| | | return BoundarySnapshotResult.failure("å½åå°åé¢ç§¯ä¸º0ï¼æ æ³ç»§ç»", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | |
| | | Device device = new Device(); |
| | | device.initFromProperties(); |
| | | String baseStationCoordinates = normalizeCoordinateValue(device.getBaseStationCoordinates()); |
| | | BaseStation baseStation = new BaseStation(); |
| | | baseStation.load(); |
| | | String baseStationCoordinates = normalizeCoordinateValue(baseStation.getInstallationCoordinates()); |
| | | if (!isMeaningfulValue(baseStationCoordinates)) { |
| | | return BoundarySnapshotResult.failure("æªè·åå°ææçåºåç«åæ ï¼è¯·å
å¨åºåç«ç®¡çä¸è®¾ç½®", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | |
| | | return BoundarySnapshotResult.failure("çæå°åè¾¹ç失败: " + ex.getMessage(), JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | |
| | | String originalBoundary = buildOriginalBoundaryString(); |
| | | String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates); |
| | | DecimalFormat areaFormat = new DecimalFormat("0.00"); |
| | | String areaString = areaFormat.format(area); |
| | | |
| | |
| | | |
| | | // å建å°åæé® |
| | | createButton.addActionListener(e -> createDikuai()); |
| | | |
| | | if (previewButton != null) { |
| | | previewButton.addActionListener(e -> previewMowingPath()); |
| | | } |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | addWindowListener(new WindowAdapter() { |
| | |
| | | if (step < 3) { |
| | | nextButton.setVisible(true); |
| | | createButton.setVisible(false); |
| | | createButton.setEnabled(false); |
| | | setPathAvailability(false); |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(false); |
| | | previewButton.setEnabled(false); |
| | | } |
| | | if (previewButtonSpacer != null) { |
| | | previewButtonSpacer.setVisible(false); |
| | | } |
| | | } else { |
| | | nextButton.setVisible(false); |
| | | createButton.setVisible(true); |
| | | createButton.setEnabled(hasGeneratedPath()); |
| | | if (previewButton != null) { |
| | | previewButton.setVisible(true); |
| | | } |
| | | if (previewButtonSpacer != null) { |
| | | previewButtonSpacer.setVisible(true); |
| | | } |
| | | setPathAvailability(hasGeneratedPath()); |
| | | } |
| | | |
| | | Container parent = prevButton.getParent(); |
| | |
| | | showStep(1); |
| | | hideBoundaryPointSummary(); |
| | | } |
| | | |
| | | restoreGeneratedPathState(session); |
| | | } |
| | | |
| | | private void restoreGeneratedPathState(DrawingSession session) { |
| | | if (session == null || session.data == null) { |
| | | showPathGenerationMessage("", true); |
| | | return; |
| | | } |
| | | |
| | | Map<String, String> data = session.data; |
| | | |
| | | if (mowingPatternCombo != null) { |
| | | String pattern = data.get("mowingPattern"); |
| | | if (pattern != null) { |
| | | ComboBoxModel<String> model = mowingPatternCombo.getModel(); |
| | | for (int i = 0; i < model.getSize(); i++) { |
| | | String candidate = model.getElementAt(i); |
| | | if (pattern.equals(candidate)) { |
| | | mowingPatternCombo.setSelectedIndex(i); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (mowingWidthSpinner != null) { |
| | | String width = data.get("mowingWidth"); |
| | | if (isMeaningfulValue(width)) { |
| | | try { |
| | | double parsed = Double.parseDouble(width.trim()); |
| | | SpinnerNumberModel model = (SpinnerNumberModel) mowingWidthSpinner.getModel(); |
| | | int min = ((Number) model.getMinimum()).intValue(); |
| | | int max = ((Number) model.getMaximum()).intValue(); |
| | | int rounded = (int) Math.round(parsed); |
| | | if (rounded < min) { |
| | | rounded = min; |
| | | } else if (rounded > max) { |
| | | rounded = max; |
| | | } |
| | | mowingWidthSpinner.setValue(rounded); |
| | | } catch (NumberFormatException ignored) { |
| | | // ä¿æå½åå¼ |
| | | } |
| | | } |
| | | } |
| | | |
| | | boolean hasPath = isMeaningfulValue(data.get("plannedPath")); |
| | | if (!hasPath) { |
| | | showPathGenerationMessage("", true); |
| | | if (currentStep == 3) { |
| | | setPathAvailability(false); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | String message = data.get(KEY_PATH_MESSAGE_TEXT); |
| | | boolean success = !"false".equalsIgnoreCase(data.get(KEY_PATH_MESSAGE_SUCCESS)); |
| | | showStep(3); |
| | | if (isMeaningfulValue(message)) { |
| | | showPathGenerationMessage(message, success); |
| | | } else { |
| | | showPathGenerationMessage("å·²çæå²èè·¯å¾ï¼å¯ç¹å»âé¢è§âæé®æ¥çææã", true); |
| | | } |
| | | setPathAvailability(true); |
| | | } |
| | | |
| | | public static void finishDrawingSession() { |
| | |
| | | Component parent = shouye != null ? shouye : null; |
| | | showAddDikuaiDialog(parent); |
| | | } |
| | | |
| | | public static void resumeFromPreview() { |
| | | Shouye shouye = Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | shouye.exitMowingPathPreview(); |
| | | } |
| | | if (activeSession == null) { |
| | | return; |
| | | } |
| | | resumeRequested = true; |
| | | Component parent = shouye != null ? shouye : null; |
| | | SwingUtilities.invokeLater(() -> showAddDikuaiDialog(parent)); |
| | | } |
| | | |
| | | private void createDikuai() { |
| | | if (!validateCurrentStep()) { |
| | |
| | | private static final double CIRCLE_SAMPLE_SIZE = 0.54d; |
| | | private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05; |
| | | private static final double BOUNDARY_CONTAINS_TOLERANCE = 0.05; |
| | | private static final double PREVIEW_BOUNDARY_MARKER_SCALE = 0.25d; |
| | | |
| | | // ç»ä»¶å¼ç¨ |
| | | private JPanel visualizationPanel; |
| | |
| | | private String currentObstacleLandNumber; |
| | | private String boundaryName; |
| | | private boolean boundaryPointsVisible; |
| | | private double boundaryPointSizeScale = 1.0d; |
| | | private boolean previewSizingEnabled; |
| | | private String currentBoundaryLandNumber; |
| | | private boolean dragInProgress; |
| | | private final Gecaoji mower; |
| | |
| | | private final List<Point2D.Double> realtimeMowingTrack = new ArrayList<>(); |
| | | private final Deque<tuowei.TrailSample> idleMowerTrail = new ArrayDeque<>(); |
| | | private final List<Point2D.Double> handheldBoundaryPreview = new ArrayList<>(); |
| | | private double boundaryPreviewMarkerScale = 1.0d; |
| | | private boolean realtimeTrackRecording; |
| | | private String realtimeTrackLandNumber; |
| | | private double mowerEffectiveWidthMeters; |
| | |
| | | drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale); |
| | | } |
| | | |
| | | adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive); |
| | | adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive, boundaryPreviewMarkerScale); |
| | | |
| | | if (hasPlannedPath) { |
| | | drawCurrentPlannedPath(g2d); |
| | | } |
| | | |
| | | if (boundaryPointsVisible && hasBoundary) { |
| | | double markerScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d); |
| | | pointandnumber.drawBoundaryPoints( |
| | | g2d, |
| | | currentBoundary, |
| | | scale, |
| | | BOUNDARY_POINT_MERGE_THRESHOLD, |
| | | BOUNDARY_POINT_COLOR |
| | | BOUNDARY_POINT_COLOR, |
| | | markerScale |
| | | ); |
| | | } |
| | | |
| | |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void clearIdleTrail() { |
| | | clearIdleMowerTrail(); |
| | | } |
| | | |
| | | public void setIdleTrailDurationSeconds(int seconds) { |
| | | int sanitized = seconds; |
| | | if (sanitized < 5 || sanitized > 600) { |
| | |
| | | } |
| | | |
| | | private void drawCurrentPlannedPath(Graphics2D g2d) { |
| | | lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale); |
| | | double arrowScale = previewSizingEnabled ? 0.5d : 1.0d; |
| | | lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale, arrowScale); |
| | | } |
| | | |
| | | private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) { |
| | |
| | | |
| | | private double computeSelectionThresholdPixels() { |
| | | double scaleFactor = Math.max(0.5, scale); |
| | | double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2); |
| | | double diameterScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d); |
| | | if (!Double.isFinite(diameterScale) || diameterScale <= 0.0d) { |
| | | diameterScale = 1.0d; |
| | | } |
| | | double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2 * diameterScale); |
| | | double markerDiameterPixels = markerDiameterWorld * scale; |
| | | return Math.max(8.0, markerDiameterPixels * 1.5); |
| | | } |
| | |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void setBoundaryPointSizeScale(double sizeScale) { |
| | | double normalized = (Double.isFinite(sizeScale) && sizeScale > 0.0d) ? sizeScale : 1.0d; |
| | | if (Math.abs(boundaryPointSizeScale - normalized) < 1e-6) { |
| | | return; |
| | | } |
| | | boundaryPointSizeScale = normalized; |
| | | if (visualizationPanel == null) { |
| | | return; |
| | | } |
| | | if (SwingUtilities.isEventDispatchThread()) { |
| | | visualizationPanel.repaint(); |
| | | } else { |
| | | SwingUtilities.invokeLater(visualizationPanel::repaint); |
| | | } |
| | | } |
| | | |
| | | public void setPathPreviewSizingEnabled(boolean enabled) { |
| | | previewSizingEnabled = enabled; |
| | | if (visualizationPanel == null) { |
| | | return; |
| | | } |
| | | if (SwingUtilities.isEventDispatchThread()) { |
| | | visualizationPanel.repaint(); |
| | | } else { |
| | | SwingUtilities.invokeLater(visualizationPanel::repaint); |
| | | } |
| | | } |
| | | |
| | | public void setBoundaryPreviewMarkerScale(double markerScale) { |
| | | double normalized = Double.isFinite(markerScale) && markerScale > 0.0d ? markerScale : 1.0d; |
| | | if (Math.abs(boundaryPreviewMarkerScale - normalized) < 1e-6) { |
| | | return; |
| | | } |
| | | boundaryPreviewMarkerScale = normalized; |
| | | if (visualizationPanel == null) { |
| | | return; |
| | | } |
| | | if (SwingUtilities.isEventDispatchThread()) { |
| | | visualizationPanel.repaint(); |
| | | } else { |
| | | SwingUtilities.invokeLater(visualizationPanel::repaint); |
| | | } |
| | | } |
| | | |
| | | public boolean setHandheldMowerIconActive(boolean handheldActive) { |
| | | if (mower == null) { |
| | | return false; |
| | | } |
| | | boolean changed = mower.useHandheldIcon(handheldActive); |
| | | if (changed && visualizationPanel != null) { |
| | | if (SwingUtilities.isEventDispatchThread()) { |
| | | visualizationPanel.repaint(); |
| | | } else { |
| | | SwingUtilities.invokeLater(visualizationPanel::repaint); |
| | | } |
| | | } |
| | | return changed; |
| | | } |
| | | |
| | | public void beginHandheldBoundaryPreview() { |
| | | handheldBoundaryPreviewActive = true; |
| | | handheldBoundaryPreview.clear(); |
| | |
| | | public void clearHandheldBoundaryPreview() { |
| | | handheldBoundaryPreviewActive = false; |
| | | handheldBoundaryPreview.clear(); |
| | | boundaryPreviewMarkerScale = 1.0d; |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | |
| | | double width = Math.max(bounds.width, 1); |
| | | double height = Math.max(bounds.height, 1); |
| | | Rectangle2D.Double targetBounds = includeMowerInBounds(bounds); |
| | | |
| | | double width = Math.max(targetBounds.width, 1); |
| | | double height = Math.max(targetBounds.height, 1); |
| | | |
| | | double targetWidth = width * 1.2; |
| | | double targetHeight = height * 1.2; |
| | |
| | | newScale = Math.max(0.05, Math.min(newScale, 50.0)); |
| | | |
| | | this.scale = newScale; |
| | | this.translateX = -bounds.getCenterX(); |
| | | this.translateY = -bounds.getCenterY(); |
| | | this.translateX = -targetBounds.getCenterX(); |
| | | this.translateY = -targetBounds.getCenterY(); |
| | | } |
| | | |
| | | // Keep the mower marker inside the viewport whenever the camera refits to scene bounds. |
| | | private Rectangle2D.Double includeMowerInBounds(Rectangle2D.Double bounds) { |
| | | Rectangle2D.Double expanded = new Rectangle2D.Double( |
| | | bounds.x, |
| | | bounds.y, |
| | | Math.max(0.0, bounds.width), |
| | | Math.max(0.0, bounds.height) |
| | | ); |
| | | |
| | | if (mower == null || !mower.hasValidPosition()) { |
| | | return expanded; |
| | | } |
| | | |
| | | Point2D.Double mowerPosition = mower.getPosition(); |
| | | if (mowerPosition == null |
| | | || !Double.isFinite(mowerPosition.x) |
| | | || !Double.isFinite(mowerPosition.y)) { |
| | | return expanded; |
| | | } |
| | | |
| | | double minX = Math.min(expanded.x, mowerPosition.x); |
| | | double minY = Math.min(expanded.y, mowerPosition.y); |
| | | double maxX = Math.max(expanded.x + expanded.width, mowerPosition.x); |
| | | double maxY = Math.max(expanded.y + expanded.height, mowerPosition.y); |
| | | |
| | | expanded.x = minX; |
| | | expanded.y = minY; |
| | | expanded.width = Math.max(0.0, maxX - minX); |
| | | expanded.height = Math.max(0.0, maxY - minY); |
| | | |
| | | return expanded; |
| | | } |
| | | |
| | | public void dispose() { |
| | |
| | | private JPanel floatingButtonPanel; |
| | | private JPanel floatingButtonColumn; |
| | | private Runnable endDrawingCallback; |
| | | private JButton pathPreviewReturnButton; |
| | | private boolean pathPreviewActive; |
| | | private Runnable pathPreviewReturnAction; |
| | | private String previewRestoreLandNumber; |
| | | private String previewRestoreLandName; |
| | | private boolean drawingPaused; |
| | | private ImageIcon pauseIcon; |
| | | private ImageIcon pauseActiveIcon; |
| | |
| | | private double[] circleBaseLatLon; |
| | | private Timer circleDataMonitor; |
| | | private Coordinate lastCapturedCoordinate; |
| | | private HandheldBoundaryCaptureDialog handheldCaptureDialog; |
| | | private boolean handheldCaptureActive; |
| | | private int handheldCapturedPoints; |
| | | private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>(); |
| | |
| | | private boolean stopButtonActive = false; |
| | | private boolean bluetoothConnected = false; |
| | | private Timer mowerSpeedRefreshTimer; |
| | | private boolean drawingControlModeActive; |
| | | private boolean storedStartButtonShowingPause; |
| | | private boolean storedStopButtonActive; |
| | | private String storedStatusBeforeDrawing; |
| | | private boolean handheldCaptureInlineUiActive; |
| | | private Timer handheldCaptureStatusTimer; |
| | | private String handheldCaptureStoredStatusText; |
| | | private Color handheldStartButtonOriginalBackground; |
| | | private Color handheldStartButtonOriginalForeground; |
| | | private Color handheldStopButtonOriginalBackground; |
| | | private Color handheldStopButtonOriginalForeground; |
| | | |
| | | public Shouye() { |
| | | instance = this; |
| | | baseStation = new BaseStation(); |
| | | baseStation.load(); |
| | | dellmessage.registerLineListener(serialLineListener); |
| | | dellmessage.registerLineListener(serialLineListener); |
| | | initializeUI(); |
| | | setupEventHandlers(); |
| | | scheduleIdentifierCheck(); |
| | | } |
| | | |
| | | public static Shouye getInstance() { |
| | |
| | | refreshMapForSelectedArea(); |
| | | } |
| | | |
| | | private void scheduleIdentifierCheck() { |
| | | HierarchyListener listener = new HierarchyListener() { |
| | | @Override |
| | | public void hierarchyChanged(HierarchyEvent e) { |
| | | if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && Shouye.this.isShowing()) { |
| | | Shouye.this.removeHierarchyListener(this); |
| | | SwingUtilities.invokeLater(() -> { |
| | | Shouye.this.checkIdentifiersAndPromptIfNeeded(); |
| | | Shouye.this.showInitialMowerSelfCheckDialogIfNeeded(); |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | addHierarchyListener(listener); |
| | | } |
| | | |
| | | private void showInitialMowerSelfCheckDialogIfNeeded() { |
| | | zijian.showInitialPromptIfNeeded(this, this::showRemoteControlDialog); |
| | | } |
| | | |
| | | private void applyIdleTrailDurationFromSettings() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | |
| | | } |
| | | if (remoteDialog != null) { |
| | | positionRemoteDialogBottomCenter(remoteDialog); |
| | | zijian.markSelfCheckCompleted(); |
| | | remoteDialog.setVisible(true); |
| | | } |
| | | remoteDialog.setVisible(true); |
| | | } |
| | | |
| | | private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) { |
| | |
| | | baseStationDialog.setVisible(true); |
| | | } |
| | | |
| | | private void checkIdentifiersAndPromptIfNeeded() { |
| | | if (baseStation == null) { |
| | | baseStation = new BaseStation(); |
| | | } |
| | | baseStation.load(); |
| | | |
| | | String currentMowerId = Setsys.getPropertyValue("mowerId"); |
| | | String currentBaseStationId = baseStation.getDeviceId(); |
| | | |
| | | if (!isIdentifierMissing(currentMowerId) && !isIdentifierMissing(currentBaseStationId)) { |
| | | return; |
| | | } |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | promptForMissingIdentifiers(owner, currentMowerId, currentBaseStationId); |
| | | } |
| | | |
| | | private void promptForMissingIdentifiers(Window owner, String currentMowerId, String currentBaseStationId) { |
| | | while (true) { |
| | | JTextField mowerField = new JTextField(10); |
| | | JTextField baseField = new JTextField(10); |
| | | |
| | | if (!isIdentifierMissing(currentMowerId)) { |
| | | mowerField.setText(currentMowerId.trim()); |
| | | } |
| | | if (!isIdentifierMissing(currentBaseStationId)) { |
| | | baseField.setText(currentBaseStationId.trim()); |
| | | } |
| | | |
| | | JPanel panel = new JPanel(); |
| | | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); |
| | | panel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); |
| | | |
| | | JLabel mowerLabel = new JLabel("å²èæºç¼å·"); |
| | | JLabel baseLabel = new JLabel("å·®ååºåç«ç¼å·"); |
| | | |
| | | mowerField.setMaximumSize(new Dimension(Integer.MAX_VALUE, mowerField.getPreferredSize().height)); |
| | | baseField.setMaximumSize(new Dimension(Integer.MAX_VALUE, baseField.getPreferredSize().height)); |
| | | |
| | | panel.add(mowerLabel); |
| | | panel.add(Box.createVerticalStrut(4)); |
| | | panel.add(mowerField); |
| | | panel.add(Box.createVerticalStrut(10)); |
| | | panel.add(baseLabel); |
| | | panel.add(Box.createVerticalStrut(4)); |
| | | panel.add(baseField); |
| | | |
| | | Object[] options = {"ä¿å", "åæ¶"}; |
| | | int result = JOptionPane.showOptionDialog(owner, panel, "å®å设å¤ä¿¡æ¯", |
| | | JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]); |
| | | |
| | | if (result != 0) { |
| | | break; |
| | | } |
| | | |
| | | String mowerInput = mowerField.getText().trim(); |
| | | String baseInput = baseField.getText().trim(); |
| | | |
| | | if (mowerInput.isEmpty()) { |
| | | JOptionPane.showMessageDialog(owner, "å²èæºç¼å·ä¸è½ä¸ºç©ºã", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | continue; |
| | | } |
| | | |
| | | if (baseInput.isEmpty()) { |
| | | JOptionPane.showMessageDialog(owner, "å·®ååºåç«ç¼å·ä¸è½ä¸ºç©ºã", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | continue; |
| | | } |
| | | |
| | | boolean mowerSaved = persistMowerIdentifier(mowerInput); |
| | | boolean baseSaved = persistBaseStationIdentifier(baseInput); |
| | | |
| | | if (mowerSaved && baseSaved) { |
| | | JOptionPane.showMessageDialog(owner, "ç¼å·å·²ä¿åã", "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | break; |
| | | } |
| | | |
| | | StringBuilder errorBuilder = new StringBuilder(); |
| | | if (!mowerSaved) { |
| | | errorBuilder.append("å²èæºç¼å·ä¿å失败ã"); |
| | | } |
| | | if (!baseSaved) { |
| | | if (errorBuilder.length() > 0) { |
| | | errorBuilder.append('\n'); |
| | | } |
| | | errorBuilder.append("å·®ååºåç«ç¼å·ä¿å失败ã"); |
| | | } |
| | | |
| | | JOptionPane.showMessageDialog(owner, errorBuilder.toString(), "ä¿å失败", JOptionPane.ERROR_MESSAGE); |
| | | |
| | | currentMowerId = Setsys.getPropertyValue("mowerId"); |
| | | baseStation.load(); |
| | | currentBaseStationId = baseStation.getDeviceId(); |
| | | } |
| | | } |
| | | |
| | | private boolean isIdentifierMissing(String value) { |
| | | if (value == null) { |
| | | return true; |
| | | } |
| | | String trimmed = value.trim(); |
| | | return trimmed.isEmpty() || "-1".equals(trimmed); |
| | | } |
| | | |
| | | private boolean persistMowerIdentifier(String mowerId) { |
| | | try { |
| | | Setsys setsys = new Setsys(); |
| | | setsys.initializeFromProperties(); |
| | | boolean updated = setsys.updateProperty("mowerId", mowerId); |
| | | if (updated) { |
| | | Device.initializeActiveDevice(mowerId); |
| | | } |
| | | return updated; |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private boolean persistBaseStationIdentifier(String baseStationId) { |
| | | if (baseStation == null) { |
| | | baseStation = new BaseStation(); |
| | | } |
| | | try { |
| | | baseStation.updateByDeviceId(baseStationId, |
| | | baseStation.getInstallationCoordinates(), |
| | | baseStation.getIotSimCardNumber(), |
| | | baseStation.getDeviceActivationTime(), |
| | | baseStation.getDataUpdateTime()); |
| | | baseStation.load(); |
| | | return true; |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private boolean hasValidBaseStationId() { |
| | | if (baseStation == null) { |
| | | return false; |
| | |
| | | } |
| | | |
| | | private void toggleStartPause() { |
| | | if (handheldCaptureInlineUiActive) { |
| | | handleHandheldConfirmAction(); |
| | | return; |
| | | } |
| | | if (drawingControlModeActive) { |
| | | toggleDrawingPause(); |
| | | return; |
| | | } |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | if (startButtonShowingPause) { |
| | | if (!zijian.ensureBeforeMowing(this, this::showRemoteControlDialog)) { |
| | | return; |
| | | } |
| | | } |
| | | startButtonShowingPause = !startButtonShowingPause; |
| | | if (!startButtonShowingPause) { |
| | | statusLabel.setText("ä½ä¸ä¸"); |
| | |
| | | } |
| | | |
| | | private void handleStopAction() { |
| | | if (handheldCaptureInlineUiActive) { |
| | | handleHandheldFinishAction(); |
| | | return; |
| | | } |
| | | if (drawingControlModeActive) { |
| | | handleDrawingStopFromControlPanel(); |
| | | return; |
| | | } |
| | | stopButtonActive = !stopButtonActive; |
| | | updateStopButtonIcon(); |
| | | if (stopButtonActive) { |
| | |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void handleDrawingStopFromControlPanel() { |
| | | if (endDrawingCallback != null) { |
| | | endDrawingCallback.run(); |
| | | } else { |
| | | addzhangaiwu.finishDrawingSession(); |
| | | } |
| | | } |
| | | |
| | | private void handleHandheldConfirmAction() { |
| | | if (!handheldCaptureInlineUiActive) { |
| | | return; |
| | | } |
| | | if (!canConfirmHandheldPoint()) { |
| | | refreshHandheldCaptureUiState(); |
| | | return; |
| | | } |
| | | int count = captureHandheldBoundaryPoint(); |
| | | if (count <= 0) { |
| | | refreshHandheldCaptureUiState(); |
| | | return; |
| | | } |
| | | refreshHandheldCaptureUiState(); |
| | | } |
| | | |
| | | private void handleHandheldFinishAction() { |
| | | if (!handheldCaptureInlineUiActive) { |
| | | return; |
| | | } |
| | | if (stopBtn != null && !stopBtn.isEnabled()) { |
| | | refreshHandheldCaptureUiState(); |
| | | return; |
| | | } |
| | | if (!finishHandheldBoundaryCapture()) { |
| | | refreshHandheldCaptureUiState(); |
| | | } |
| | | } |
| | | |
| | | private void enterHandheldCaptureInlineUi() { |
| | | if (handheldCaptureInlineUiActive) { |
| | | refreshHandheldCaptureUiState(); |
| | | return; |
| | | } |
| | | handheldCaptureInlineUiActive = true; |
| | | handheldCaptureStoredStatusText = statusLabel != null ? statusLabel.getText() : null; |
| | | if (statusLabel != null) { |
| | | statusLabel.setText("ææééä¸"); |
| | | } |
| | | if (startBtn != null) { |
| | | handheldStartButtonOriginalBackground = startBtn.getBackground(); |
| | | handheldStartButtonOriginalForeground = startBtn.getForeground(); |
| | | startBtn.setIcon(null); |
| | | startBtn.setIconTextGap(0); |
| | | startBtn.setHorizontalAlignment(SwingConstants.CENTER); |
| | | startBtn.setHorizontalTextPosition(SwingConstants.CENTER); |
| | | startBtn.setVerticalTextPosition(SwingConstants.CENTER); |
| | | } |
| | | if (stopBtn != null) { |
| | | handheldStopButtonOriginalBackground = stopBtn.getBackground(); |
| | | handheldStopButtonOriginalForeground = stopBtn.getForeground(); |
| | | stopBtn.setIcon(null); |
| | | stopBtn.setIconTextGap(0); |
| | | stopBtn.setHorizontalAlignment(SwingConstants.CENTER); |
| | | stopBtn.setHorizontalTextPosition(SwingConstants.CENTER); |
| | | stopBtn.setVerticalTextPosition(SwingConstants.CENTER); |
| | | stopBtn.setText("ç»æ"); |
| | | } |
| | | startHandheldCaptureStatusTimer(); |
| | | refreshHandheldCaptureUiState(); |
| | | } |
| | | |
| | | private void exitHandheldCaptureInlineUi() { |
| | | if (!handheldCaptureInlineUiActive) { |
| | | return; |
| | | } |
| | | handheldCaptureInlineUiActive = false; |
| | | stopHandheldCaptureStatusTimer(); |
| | | if (statusLabel != null) { |
| | | statusLabel.setText(handheldCaptureStoredStatusText != null ? handheldCaptureStoredStatusText : "å¾
æº"); |
| | | } |
| | | if (startBtn != null) { |
| | | startBtn.setToolTipText(null); |
| | | if (handheldStartButtonOriginalBackground != null) { |
| | | startBtn.setBackground(handheldStartButtonOriginalBackground); |
| | | } |
| | | if (handheldStartButtonOriginalForeground != null) { |
| | | startBtn.setForeground(handheldStartButtonOriginalForeground); |
| | | } |
| | | startBtn.setEnabled(true); |
| | | updateStartButtonAppearance(); |
| | | } |
| | | if (stopBtn != null) { |
| | | stopBtn.setToolTipText(null); |
| | | if (handheldStopButtonOriginalBackground != null) { |
| | | stopBtn.setBackground(handheldStopButtonOriginalBackground); |
| | | } |
| | | if (handheldStopButtonOriginalForeground != null) { |
| | | stopBtn.setForeground(handheldStopButtonOriginalForeground); |
| | | } |
| | | stopBtn.setEnabled(true); |
| | | stopBtn.setText("ç»æ"); |
| | | updateStopButtonIcon(); |
| | | } |
| | | handheldCaptureStoredStatusText = null; |
| | | handheldStartButtonOriginalBackground = null; |
| | | handheldStartButtonOriginalForeground = null; |
| | | handheldStopButtonOriginalBackground = null; |
| | | handheldStopButtonOriginalForeground = null; |
| | | } |
| | | |
| | | private void startHandheldCaptureStatusTimer() { |
| | | if (handheldCaptureStatusTimer == null) { |
| | | handheldCaptureStatusTimer = new Timer(400, e -> refreshHandheldCaptureUiState()); |
| | | handheldCaptureStatusTimer.setRepeats(true); |
| | | } |
| | | if (!handheldCaptureStatusTimer.isRunning()) { |
| | | handheldCaptureStatusTimer.start(); |
| | | } |
| | | } |
| | | |
| | | private void stopHandheldCaptureStatusTimer() { |
| | | if (handheldCaptureStatusTimer != null && handheldCaptureStatusTimer.isRunning()) { |
| | | handheldCaptureStatusTimer.stop(); |
| | | } |
| | | } |
| | | |
| | | // Update inline handheld capture buttons based on the current device reading. |
| | | private void refreshHandheldCaptureUiState() { |
| | | if (!handheldCaptureInlineUiActive) { |
| | | return; |
| | | } |
| | | int nextIndex = handheldCapturedPoints + 1; |
| | | boolean hasFix = hasHighPrecisionFix(); |
| | | boolean hasValid = hasValidRealtimeHandheldPosition(); |
| | | boolean duplicate = hasValid && isCurrentHandheldPointDuplicate(); |
| | | boolean canConfirm = handheldCaptureActive && hasFix && hasValid && !duplicate; |
| | | |
| | | if (startBtn != null) { |
| | | String prompt = "<html><center>ééç¹" + nextIndex + "<br>ç¡®å®</center></html>"; |
| | | startBtn.setText(prompt); |
| | | startBtn.setEnabled(canConfirm); |
| | | if (canConfirm) { |
| | | if (handheldStartButtonOriginalBackground != null) { |
| | | startBtn.setBackground(handheldStartButtonOriginalBackground); |
| | | } |
| | | if (handheldStartButtonOriginalForeground != null) { |
| | | startBtn.setForeground(handheldStartButtonOriginalForeground); |
| | | } |
| | | startBtn.setToolTipText(null); |
| | | } else { |
| | | startBtn.setBackground(new Color(200, 200, 200)); |
| | | startBtn.setForeground(new Color(130, 130, 130)); |
| | | startBtn.setToolTipText(resolveHandheldConfirmTooltip(hasFix, hasValid, duplicate)); |
| | | } |
| | | } |
| | | |
| | | if (stopBtn != null) { |
| | | boolean canFinish = handheldCapturedPoints >= 3; |
| | | stopBtn.setText("ç»æ"); |
| | | stopBtn.setEnabled(canFinish); |
| | | if (canFinish) { |
| | | if (handheldStopButtonOriginalBackground != null) { |
| | | stopBtn.setBackground(handheldStopButtonOriginalBackground); |
| | | } |
| | | if (handheldStopButtonOriginalForeground != null) { |
| | | stopBtn.setForeground(handheldStopButtonOriginalForeground); |
| | | } |
| | | stopBtn.setToolTipText("ç»æééå¹¶è¿åæ°å¢å°å"); |
| | | } else { |
| | | stopBtn.setBackground(new Color(220, 220, 220)); |
| | | stopBtn.setForeground(new Color(130, 130, 130)); |
| | | stopBtn.setToolTipText("è³å°ééä¸ä¸ªç¹æè½ç»æ"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String resolveHandheldConfirmTooltip(boolean hasFix, boolean hasValidPosition, boolean duplicate) { |
| | | if (!hasFix) { |
| | | return "å½åå®ä½è´¨éä¸è¶³ï¼æ æ³éé"; |
| | | } |
| | | if (!hasValidPosition) { |
| | | return "å½åå®ä½æ°æ®æ æï¼è¯·ç¨ååè¯"; |
| | | } |
| | | if (duplicate) { |
| | | return "å½ååæ å·²ééï¼è¯·ç§»å¨å°æ°çä½ç½®"; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean hasHighPrecisionFix() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | String status = device.getPositioningStatus(); |
| | | return status != null && "4".equals(status.trim()); |
| | | } |
| | | |
| | | private boolean canConfirmHandheldPoint() { |
| | | return handheldCaptureActive |
| | | && hasHighPrecisionFix() |
| | | && hasValidRealtimeHandheldPosition() |
| | | && !isCurrentHandheldPointDuplicate(); |
| | | } |
| | | |
| | | private void enterDrawingControlMode() { |
| | | if (drawingControlModeActive) { |
| | | return; |
| | | } |
| | | storedStartButtonShowingPause = startButtonShowingPause; |
| | | storedStopButtonActive = stopButtonActive; |
| | | storedStatusBeforeDrawing = statusLabel != null ? statusLabel.getText() : null; |
| | | drawingControlModeActive = true; |
| | | applyDrawingPauseState(false, false); |
| | | updateDrawingControlButtonLabels(); |
| | | } |
| | | |
| | | private void exitDrawingControlMode() { |
| | | if (!drawingControlModeActive) { |
| | | return; |
| | | } |
| | | drawingControlModeActive = false; |
| | | applyDrawingPauseState(false, false); |
| | | drawingPaused = false; |
| | | stopButtonActive = storedStopButtonActive; |
| | | startButtonShowingPause = storedStartButtonShowingPause; |
| | | if (startBtn != null) { |
| | | updateStartButtonAppearance(); |
| | | } |
| | | if (stopBtn != null) { |
| | | stopBtn.setText("ç»æ"); |
| | | updateStopButtonIcon(); |
| | | } |
| | | if (statusLabel != null) { |
| | | statusLabel.setText(storedStatusBeforeDrawing != null ? storedStatusBeforeDrawing : "å¾
æº"); |
| | | } |
| | | storedStatusBeforeDrawing = null; |
| | | } |
| | | |
| | | private void updateDrawingControlButtonLabels() { |
| | | if (!drawingControlModeActive) { |
| | | return; |
| | | } |
| | | configureButtonForDrawingMode(startBtn); |
| | | configureButtonForDrawingMode(stopBtn); |
| | | if (startBtn != null) { |
| | | startBtn.setText(drawingPaused ? "å¼å§ç»å¶" : "æåç»å¶"); |
| | | } |
| | | if (stopBtn != null) { |
| | | stopBtn.setText("ç»æç»å¶"); |
| | | } |
| | | } |
| | | |
| | | private void configureButtonForDrawingMode(JButton button) { |
| | | if (button == null) { |
| | | return; |
| | | } |
| | | button.setIcon(null); |
| | | button.setIconTextGap(0); |
| | | button.setHorizontalAlignment(SwingConstants.CENTER); |
| | | button.setHorizontalTextPosition(SwingConstants.CENTER); |
| | | } |
| | | |
| | | private void updateStartButtonAppearance() { |
| | | if (startBtn == null) { |
| | | return; |
| | |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | public void setHandheldMowerIconActive(boolean active) { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.setHandheldMowerIconActive(active); |
| | | } |
| | | |
| | | public boolean startMowerBoundaryCapture() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | |
| | | return false; |
| | | } |
| | | |
| | | mapRenderer.clearIdleTrail(); |
| | | |
| | | activeBoundaryMode = BoundaryCaptureMode.MOWER; |
| | | mowerBoundaryCaptureActive = true; |
| | | mowerBaseLatLon = baseLatLonCandidate; |
| | |
| | | Coordinate.setStartSaveGngga(true); |
| | | |
| | | if (mapRenderer != null) { |
| | | mapRenderer.setBoundaryPreviewMarkerScale(2.0d); |
| | | mapRenderer.beginHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | setHandheldMowerIconActive(false); |
| | | |
| | | startMowerBoundaryMonitor(); |
| | | return true; |
| | | } |
| | |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | if (handheldCaptureDialog != null && handheldCaptureDialog.isShowing()) { |
| | | handheldCaptureDialog.toFront(); |
| | | return true; |
| | | } |
| | | |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } |
| | | |
| | | mapRenderer.clearIdleTrail(); |
| | | |
| | | activeBoundaryMode = BoundaryCaptureMode.HANDHELD; |
| | | handheldCaptureActive = true; |
| | | handheldCapturedPoints = 0; |
| | |
| | | handheldTemporaryPoints.clear(); |
| | | } |
| | | AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList()); |
| | | mapRenderer.beginHandheldBoundaryPreview(); |
| | | |
| | | Window ownerWindow = SwingUtilities.getWindowAncestor(this); |
| | | SwingUtilities.invokeLater(() -> { |
| | | Window targetOwner = ownerWindow; |
| | | if (targetOwner == null) { |
| | | targetOwner = SwingUtilities.getWindowAncestor(Shouye.this); |
| | | } |
| | | HandheldBoundaryCaptureDialog dialog = new HandheldBoundaryCaptureDialog(targetOwner, Shouye.this, visualizationPanel, THEME_COLOR); |
| | | handheldCaptureDialog = dialog; |
| | | dialog.setVisible(true); |
| | | }); |
| | | |
| | | mapRenderer.setBoundaryPreviewMarkerScale(1.0d); |
| | | mapRenderer.beginHandheldBoundaryPreview(); |
| | | setHandheldMowerIconActive(true); |
| | | enterHandheldCaptureInlineUi(); |
| | | return true; |
| | | } |
| | | |
| | |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | setHandheldMowerIconActive(false); |
| | | } |
| | | |
| | | private void discardLatestCoordinate(Coordinate coordinate) { |
| | |
| | | |
| | | List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot(); |
| | | handheldCaptureActive = false; |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | |
| | | |
| | | AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot); |
| | | |
| | | exitHandheldCaptureInlineUi(); |
| | | |
| | | SwingUtilities.invokeLater(AddDikuai::finishDrawingSession); |
| | | return true; |
| | | } |
| | | |
| | | void handheldBoundaryCaptureDialogClosed(HandheldBoundaryCaptureDialog dialog) { |
| | | if (handheldCaptureDialog == dialog) { |
| | | handheldCaptureDialog = null; |
| | | } |
| | | handheldCaptureActive = false; |
| | | if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | } |
| | | |
| | | int getHandheldCapturedPointCount() { |
| | | return handheldCapturedPoints; |
| | | } |
| | |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if ("ä½ä¸ä¸".equals(statusText)) { |
| | | if ("ä½ä¸ä¸".equals(statusText) || "ç»å¶ä¸".equals(statusText)) { |
| | | statusLabel.setForeground(THEME_COLOR); |
| | | } else if ("æåä¸".equals(statusText)) { |
| | | } else if ("æåä¸".equals(statusText) || "ç»å¶æå".equals(statusText)) { |
| | | statusLabel.setForeground(STATUS_PAUSE_COLOR); |
| | | } else { |
| | | statusLabel.setForeground(Color.GRAY); |
| | |
| | | return button; |
| | | } |
| | | |
| | | private JButton createFloatingTextButton(String text) { |
| | | JButton button = new JButton(text); |
| | | button.setFont(new Font("微软é
é»", Font.BOLD, 15)); |
| | | button.setForeground(Color.WHITE); |
| | | button.setBackground(THEME_COLOR); |
| | | button.setBorder(BorderFactory.createEmptyBorder(10, 18, 10, 18)); |
| | | button.setFocusPainted(false); |
| | | button.setOpaque(true); |
| | | button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | button.addMouseListener(new MouseAdapter() { |
| | | @Override |
| | | public void mouseEntered(MouseEvent e) { |
| | | button.setBackground(THEME_HOVER_COLOR); |
| | | } |
| | | |
| | | @Override |
| | | public void mouseExited(MouseEvent e) { |
| | | button.setBackground(THEME_COLOR); |
| | | } |
| | | }); |
| | | return button; |
| | | } |
| | | |
| | | private ImageIcon loadScaledIcon(String path, int width, int height) { |
| | | try { |
| | | ImageIcon icon = new ImageIcon(path); |
| | |
| | | if (notifyCoordinate) { |
| | | Coordinate.setStartSaveGngga(!paused); |
| | | } |
| | | if (drawingControlModeActive) { |
| | | updateDrawingControlButtonLabels(); |
| | | if (statusLabel != null) { |
| | | statusLabel.setText(paused ? "ç»å¶æå" : "ç»å¶ä¸"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void toggleDrawingPause() { |
| | |
| | | |
| | | public void showEndDrawingButton(Runnable callback, String drawingShape) { |
| | | endDrawingCallback = callback; |
| | | applyDrawingPauseState(false, false); |
| | | circleDialogMode = false; |
| | | hideCircleGuidancePanel(); |
| | | |
| | | ensureFloatingIconsLoaded(); |
| | | ensureFloatingButtonInfrastructure(); |
| | | enterDrawingControlMode(); |
| | | |
| | | boolean enableCircleGuidance = drawingShape != null |
| | | && "circle".equalsIgnoreCase(drawingShape.trim()); |
| | | if (enableCircleGuidance) { |
| | | prepareCircleGuidanceState(); |
| | | showCircleGuidanceStep(1); |
| | | endDrawingButton.setVisible(false); |
| | | ensureFloatingIconsLoaded(); |
| | | ensureFloatingButtonInfrastructure(); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | if (endDrawingButton != null) { |
| | | endDrawingButton.setVisible(false); |
| | | } |
| | | prepareCircleGuidanceState(); |
| | | showCircleGuidanceStep(1); |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | rebuildFloatingButtonColumn(); |
| | | } else { |
| | | clearCircleGuidanceArtifacts(); |
| | | endDrawingButton.setVisible(true); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(true); |
| | | } |
| | | hideFloatingDrawingControls(); |
| | | } |
| | | |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | |
| | | rebuildFloatingButtonColumn(); |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | |
| | | floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | } |
| | | floatingButtonColumn.add(endDrawingButton); |
| | | added = true; |
| | | } |
| | | if (pathPreviewReturnButton != null && pathPreviewReturnButton.isVisible()) { |
| | | if (added) { |
| | | floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | } |
| | | floatingButtonColumn.add(pathPreviewReturnButton); |
| | | added = true; |
| | | } |
| | | floatingButtonColumn.revalidate(); |
| | | floatingButtonColumn.repaint(); |
| | |
| | | |
| | | private double[] resolveCircleBaseLatLon() { |
| | | String coords = null; |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | coords = current.getBaseStationCoordinates(); |
| | | |
| | | if (baseStation == null) { |
| | | baseStation = new BaseStation(); |
| | | } |
| | | baseStation.load(); |
| | | coords = baseStation.getInstallationCoordinates(); |
| | | |
| | | if (!isMeaningfulValue(coords)) { |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | coords = current.getBaseStationCoordinates(); |
| | | } |
| | | } |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | coords = addzhangaiwu.getActiveSessionBaseStation(); |
| | | } |
| | | if (!isMeaningfulValue(coords) && baseStation != null) { |
| | | coords = baseStation.getInstallationCoordinates(); |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | return null; |
| | | if (!isMeaningfulValue(coords)) { |
| | | coords = addzhangaiwu.getActiveSessionBaseStation(); |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | return null; |
| | | } |
| | | } |
| | | String[] parts = coords.split(","); |
| | | if (parts.length < 4) { |
| | |
| | | clearCircleGuidanceArtifacts(); |
| | | hideFloatingDrawingControls(); |
| | | circleDialogMode = false; |
| | | applyDrawingPauseState(false, false); |
| | | exitHandheldCaptureInlineUi(); |
| | | handheldCaptureActive = false; |
| | | exitDrawingControlMode(); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) { |
| | |
| | | endDrawingCallback = null; |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | setHandheldMowerIconActive(false); |
| | | } |
| | | |
| | | private void showPathPreviewReturnControls() { |
| | | ensureFloatingButtonInfrastructure(); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | if (endDrawingButton != null) { |
| | | endDrawingButton.setVisible(false); |
| | | } |
| | | if (pathPreviewReturnButton == null) { |
| | | pathPreviewReturnButton = createFloatingTextButton("è¿å"); |
| | | pathPreviewReturnButton.setToolTipText("è¿åæ°å¢å°åæ¥éª¤"); |
| | | pathPreviewReturnButton.addActionListener(e -> handlePathPreviewReturn()); |
| | | } |
| | | pathPreviewReturnButton.setVisible(true); |
| | | if (floatingButtonPanel != null) { |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | } |
| | | rebuildFloatingButtonColumn(); |
| | | } |
| | | |
| | | private void hidePathPreviewReturnControls() { |
| | | if (pathPreviewReturnButton != null) { |
| | | pathPreviewReturnButton.setVisible(false); |
| | | } |
| | | rebuildFloatingButtonColumn(); |
| | | if (floatingButtonPanel != null && floatingButtonColumn != null |
| | | && floatingButtonColumn.getComponentCount() == 0) { |
| | | floatingButtonPanel.setVisible(false); |
| | | } |
| | | } |
| | | |
| | | private void handlePathPreviewReturn() { |
| | | Runnable callback = pathPreviewReturnAction; |
| | | exitMowingPathPreview(); |
| | | if (callback != null) { |
| | | callback.run(); |
| | | } |
| | | } |
| | | |
| | | public boolean startMowingPathPreview(String landNumber, |
| | | String landName, |
| | | String boundary, |
| | | String obstacles, |
| | | String plannedPath, |
| | | Runnable returnAction) { |
| | | if (mapRenderer == null || !isMeaningfulValue(plannedPath)) { |
| | | return false; |
| | | } |
| | | |
| | | if (pathPreviewActive) { |
| | | exitMowingPathPreview(); |
| | | } |
| | | |
| | | exitDrawingControlMode(); |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | |
| | | pathPreviewReturnAction = returnAction; |
| | | pathPreviewActive = true; |
| | | mapRenderer.setPathPreviewSizingEnabled(true); |
| | | |
| | | previewRestoreLandNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | previewRestoreLandName = null; |
| | | if (isMeaningfulValue(previewRestoreLandNumber)) { |
| | | Dikuai existing = Dikuai.getDikuai(previewRestoreLandNumber); |
| | | if (existing != null) { |
| | | previewRestoreLandName = existing.getLandName(); |
| | | } |
| | | } |
| | | |
| | | mapRenderer.setCurrentBoundary(boundary, landNumber, landName); |
| | | mapRenderer.setCurrentObstacles(obstacles, landNumber); |
| | | mapRenderer.setCurrentPlannedPath(plannedPath); |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | mapRenderer.setBoundaryPointSizeScale(1.0d); |
| | | mapRenderer.setBoundaryPointsVisible(isMeaningfulValue(boundary)); |
| | | |
| | | String displayName = isMeaningfulValue(landName) ? landName : landNumber; |
| | | updateCurrentAreaName(displayName); |
| | | |
| | | showPathPreviewReturnControls(); |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | return true; |
| | | } |
| | | |
| | | public void exitMowingPathPreview() { |
| | | if (!pathPreviewActive) { |
| | | return; |
| | | } |
| | | pathPreviewActive = false; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.setPathPreviewSizingEnabled(false); |
| | | } |
| | | hidePathPreviewReturnControls(); |
| | | |
| | | String restoreNumber = previewRestoreLandNumber; |
| | | String restoreName = previewRestoreLandName; |
| | | previewRestoreLandNumber = null; |
| | | previewRestoreLandName = null; |
| | | pathPreviewReturnAction = null; |
| | | |
| | | if (restoreNumber != null) { |
| | | Dikuaiguanli.setCurrentWorkLand(restoreNumber, restoreName); |
| | | } else if (mapRenderer != null) { |
| | | mapRenderer.setCurrentBoundary(null, null, null); |
| | | mapRenderer.setCurrentObstacles((String) null, null); |
| | | mapRenderer.setCurrentPlannedPath(null); |
| | | mapRenderer.setBoundaryPointsVisible(false); |
| | | mapRenderer.setBoundaryPointSizeScale(1.0d); |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | mapRenderer.resetView(); |
| | | updateCurrentAreaName(null); |
| | | } |
| | | |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | private void initializeDefaultAreaSelection() { |
| | | Dikuai.initFromProperties(); |
| | | String persistedLandNumber = Dikuaiguanli.getPersistedWorkLandNumber(); |
| | | if (persistedLandNumber != null) { |
| | | Dikuai stored = Dikuai.getDikuai(persistedLandNumber); |
| | | if (stored != null) { |
| | | Dikuaiguanli.setCurrentWorkLand(persistedLandNumber, stored.getLandName()); |
| | | return; |
| | | } |
| | | Dikuaiguanli.setCurrentWorkLand(null, null); |
| | | } |
| | | |
| | | Map<String, Dikuai> all = Dikuai.getAllDikuai(); |
| | | if (all.isEmpty()) { |
| | | Dikuaiguanli.setCurrentWorkLand(null, null); |
| | |
| | | private static final Color HANDHELD_BOUNDARY_FILL = new Color(51, 153, 255, 60); |
| | | private static final Color HANDHELD_BOUNDARY_BORDER = new Color(0, 100, 0, 220); |
| | | private static final Color HANDHELD_BOUNDARY_POINT = new Color(0, 100, 0); |
| | | private static final double BASE_WORLD_MARKER_SIZE = 0.27d; // halve the base diameter for subtler markers |
| | | private static final double MIN_PIXEL_DIAMETER = 3.0d; |
| | | private static final double MAX_PIXEL_DIAMETER = 9.0d; |
| | | private static volatile double cachedMarkerPixelDiameter = -1.0d; |
| | | |
| | | private adddikuaiyulan() { |
| | | } |
| | |
| | | public static void drawPreview(Graphics2D g2d, |
| | | List<Point2D.Double> previewPoints, |
| | | double scale, |
| | | boolean previewActive) { |
| | | boolean previewActive, |
| | | double diameterScale) { |
| | | if (!previewActive) { |
| | | cachedMarkerPixelDiameter = -1.0d; |
| | | } |
| | | |
| | | if (g2d == null || !previewActive || previewPoints == null || previewPoints.isEmpty()) { |
| | | return; |
| | | } |
| | |
| | | g2d.fill(path); |
| | | } |
| | | |
| | | float outlineWidth =0.1f; |
| | | float outlineWidth = 0.1f; |
| | | g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | g2d.draw(path); |
| | | |
| | | double markerSize = 0.2d; |
| | | double markerRadius = markerSize / 2.0d; |
| | | if (cachedMarkerPixelDiameter <= 0.0d) { |
| | | double previousPixelDiameter = Math.abs(BASE_WORLD_MARKER_SIZE * scale); |
| | | if (previousPixelDiameter <= 0.0d) { |
| | | previousPixelDiameter = MIN_PIXEL_DIAMETER; |
| | | } |
| | | cachedMarkerPixelDiameter = Math.max(MIN_PIXEL_DIAMETER, |
| | | Math.min(MAX_PIXEL_DIAMETER, previousPixelDiameter)); |
| | | } |
| | | |
| | | double effectiveScale = Math.max(0.01d, scale); |
| | | double markerSize = cachedMarkerPixelDiameter / effectiveScale; |
| | | double normalizedScale = Double.isFinite(diameterScale) && diameterScale > 0.0d ? diameterScale : 1.0d; |
| | | markerSize *= normalizedScale; |
| | | double markerRadius = markerSize / 2.0d; |
| | | |
| | | for (Point2D.Double point : previewPoints) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | |
| | | List<Point2D.Double> boundary, // è¾¹çç¹éå |
| | | double scale, // ç¼©æ¾æ¯ä¾ |
| | | double mergeThreshold, // åå¹¶éå¼ |
| | | Color pointColor) { // ç¹é¢è² |
| | | Color pointColor, // ç¹é¢è² |
| | | double diameterScale) { // ç´å¾ç¼©æ¾å å |
| | | if (boundary == null || boundary.size() < 2) { // å¤ææ°æ®æ¯å¦ææ |
| | | return; // æ æç´æ¥è¿å |
| | | } |
| | |
| | | return; // æ æè¿å |
| | | } |
| | | |
| | | double scaleFactor = Math.max(0.5, scale); // 鲿¢è¿å°ç¼©æ¾ |
| | | double markerDiameter = Math.max(1.0, (10.0 / scaleFactor) * 0.2); // æç¹ç´å¾ |
| | | double scaleFactor = Math.max(0.5, scale); // 鲿¢è¿å°ç¼©æ¾ |
| | | double clampedScale = diameterScale > 0 ? diameterScale : 1.0; // 鲿¢éæ³ç¼©æ¾ |
| | | double minimumDiameter = clampedScale < 1.0 ? 0.5 : 1.0; // ç¼©å°æ¶å
许æ´å°çæå°å¼ |
| | | double markerDiameter = Math.max(minimumDiameter, (10.0 / scaleFactor) * 0.2 * clampedScale); // æç¹ç´å¾ |
| | | double markerRadius = markerDiameter / 2.0; // åå¾ |
| | | |
| | | for (int i = 0; i < effectiveCount; i++) { // éåææç¹ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package zhuye; |
| | | |
| | | import javax.swing.JDialog; |
| | | import javax.swing.JOptionPane; |
| | | import javax.swing.SwingUtilities; |
| | | import java.awt.Component; |
| | | import java.awt.Dimension; |
| | | import java.awt.Toolkit; |
| | | import java.awt.Window; |
| | | |
| | | /** |
| | | * èªæ£æç¤ºå·¥å
·ç±»ï¼éä¸ç®¡çå²èæºä½ä¸åçèªæ£æç¤ºé»è¾ã |
| | | */ |
| | | public final class zijian { |
| | | private static boolean selfCheckCompleted; |
| | | private static boolean initialPromptShown; |
| | | |
| | | private zijian() { |
| | | // Utility class |
| | | } |
| | | |
| | | public static boolean ensureBeforeMowing(Component anchorComponent, Runnable onSelfCheckConfirmed) { |
| | | if (selfCheckCompleted) { |
| | | return true; |
| | | } |
| | | showSelfCheckDialog(anchorComponent, onSelfCheckConfirmed); |
| | | return false; |
| | | } |
| | | |
| | | public static void showInitialPromptIfNeeded(Component anchorComponent, Runnable onSelfCheckConfirmed) { |
| | | if (selfCheckCompleted || initialPromptShown) { |
| | | return; |
| | | } |
| | | initialPromptShown = true; |
| | | showSelfCheckDialog(anchorComponent, onSelfCheckConfirmed); |
| | | } |
| | | |
| | | public static void markSelfCheckCompleted() { |
| | | selfCheckCompleted = true; |
| | | } |
| | | |
| | | private static void showSelfCheckDialog(Component anchorComponent, Runnable onSelfCheckConfirmed) { |
| | | Component parent = resolveDialogParent(anchorComponent); |
| | | Object[] options = {"ç«å³èªæ£", "åæ¶"}; |
| | | String message = "<html>å²èå请å
宿坹å²èæºçèªæ£æä½ã<br>饿§å²èæºååå¼çº¦2ç±³ï¼ç¶ååå°è½¬åå®æèªæ£åè½ã</html>"; |
| | | |
| | | JOptionPane optionPane = new JOptionPane( |
| | | message, |
| | | JOptionPane.INFORMATION_MESSAGE, |
| | | JOptionPane.DEFAULT_OPTION, |
| | | null, |
| | | options, |
| | | options[0]); |
| | | |
| | | JDialog dialog = optionPane.createDialog(parent, "èªæ£æç¤º"); |
| | | dialog.pack(); |
| | | applyTargetWidth(anchorComponent, dialog); |
| | | dialog.setLocationRelativeTo(parent instanceof Component ? (Component) parent : null); |
| | | dialog.setVisible(true); |
| | | |
| | | Object selectedValue = optionPane.getValue(); |
| | | dialog.dispose(); |
| | | |
| | | boolean confirmed = options[0].equals(selectedValue); |
| | | if (confirmed) { |
| | | selfCheckCompleted = true; |
| | | if (onSelfCheckConfirmed != null) { |
| | | SwingUtilities.invokeLater(onSelfCheckConfirmed); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static void applyTargetWidth(Component anchorComponent, JDialog dialog) { |
| | | if (dialog == null) { |
| | | return; |
| | | } |
| | | int targetWidth = resolveTargetDialogWidth(anchorComponent); |
| | | if (targetWidth <= 0) { |
| | | return; |
| | | } |
| | | Dimension currentSize = dialog.getSize(); |
| | | if (currentSize == null) { |
| | | currentSize = dialog.getPreferredSize(); |
| | | } |
| | | if (currentSize == null) { |
| | | currentSize = new Dimension(targetWidth, 0); |
| | | } |
| | | int width = Math.max(targetWidth, currentSize.width); |
| | | int height = currentSize.height > 0 ? currentSize.height : dialog.getHeight(); |
| | | if (height <= 0) { |
| | | height = 200; |
| | | } |
| | | dialog.setSize(new Dimension(width, height)); |
| | | } |
| | | |
| | | private static int resolveTargetDialogWidth(Component anchorComponent) { |
| | | int baseWidth = 0; |
| | | if (anchorComponent != null) { |
| | | baseWidth = anchorComponent.getWidth(); |
| | | if (baseWidth <= 0) { |
| | | Component parent = resolveDialogParent(anchorComponent); |
| | | if (parent != null) { |
| | | baseWidth = parent.getWidth(); |
| | | } |
| | | } |
| | | } |
| | | if (baseWidth <= 0) { |
| | | Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); |
| | | baseWidth = screenSize != null ? screenSize.width : 0; |
| | | } |
| | | if (baseWidth <= 0) { |
| | | return 0; |
| | | } |
| | | return (int) Math.round(baseWidth * 0.8); |
| | | } |
| | | |
| | | private static Component resolveDialogParent(Component anchorComponent) { |
| | | if (anchorComponent == null) { |
| | | return null; |
| | | } |
| | | if (anchorComponent instanceof Window) { |
| | | return anchorComponent; |
| | | } |
| | | Window window = SwingUtilities.getWindowAncestor(anchorComponent); |
| | | return window != null ? window : anchorComponent; |
| | | } |
| | | } |
| | |
| | | #Updated User Properties |
| | | #Sun Nov 23 11:24:29 GMT+08:00 2025 |
| | | registrationTime=-1 |
| | | #Tue Dec 09 17:21:03 CST 2025 |
| | | email=789 |
| | | language=zh |
| | | lastLoginTime=-1 |
| | | password=123 |
| | | language=zh |
| | | userName=233 |
| | | userId=-1 |
| | | email=789 |
| | | registrationTime=-1 |
| | | status=-1 |
| | | userId=-1 |
| | | userName=233 |