From 0930bed760105b81e2e5055801bec6d6e8d57358 Mon Sep 17 00:00:00 2001
From: 张世豪 <979909237@qq.com>
Date: 星期二, 23 十二月 2025 18:40:08 +0800
Subject: [PATCH] 新增了功能
---
src/lujing/AoxinglujingNoObstacle.java | 457 ++++++++++++++++++++++----------------------------------
1 files changed, 183 insertions(+), 274 deletions(-)
diff --git a/src/lujing/AoxinglujingNoObstacle.java b/src/lujing/AoxinglujingNoObstacle.java
index 75717ba..fe884f9 100644
--- a/src/lujing/AoxinglujingNoObstacle.java
+++ b/src/lujing/AoxinglujingNoObstacle.java
@@ -1,331 +1,240 @@
package lujing;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
- * 鏃犻殰纰嶇墿鍑稿舰鑽夊湴璺緞瑙勫垝绫� (浼樺寲鐗�)
+ * 鍑稿舰鑽夊湴璺緞瑙勫垝 (鍥磋竟浼樺寲鐗�)
+ * 浼樺寲閲嶇偣锛氬洿杈瑰潗鏍囧榻愭壂鎻忚捣鐐广�佸叏璺緞杩炶疮鎬�
*/
public class AoxinglujingNoObstacle {
- // 浼樺寲锛氬紩鍏ユ瀬灏忓�肩敤浜庢诞鐐规暟姣旇緝锛屽鐞嗗嚑浣曠簿搴﹁宸�
private static final double EPSILON = 1e-6;
- /**
- * 璺緞娈电被
- */
- public static class PathSegment {
- public Point start;
- public Point end;
- public boolean isMowing; // true涓哄壊鑽夊伐浣滄锛宖alse涓鸿繃娓℃
-
- public PathSegment(Point start, Point end, boolean isMowing) {
- this.start = start;
- this.end = end;
- this.isMowing = isMowing;
- }
-
- @Override
- public String toString() {
- return String.format("[%s -> %s, isMowing=%b]", start, end, isMowing);
- }
- }
-
- /**
- * 鍧愭爣鐐圭被
- */
public static class Point {
- double x, y;
-
- public Point(double x, double y) {
- this.x = x;
- this.y = y;
- }
-
+ public double x, y;
+ public Point(double x, double y) { this.x = x; this.y = y; }
@Override
- public String toString() {
- return String.format("(%.4f, %.4f)", x, y);
+ public String toString() { return String.format("%.6f,%.6f", x, y); }
+ }
+
+ public static class PathSegment {
+ public Point start, end;
+ public boolean isMowing;
+ public PathSegment(Point start, Point end, boolean isMowing) {
+ this.start = start; this.end = end; this.isMowing = isMowing;
}
}
/**
- * 瀵瑰鍏紑鐨勯潤鎬佽皟鐢ㄦ柟娉� (淇濈暀瀛楃涓插叆鍙傛牸寮�)
- *
- * @param boundaryCoordsStr 鍦板潡杈圭晫鍧愭爣瀛楃涓� "x1,y1;x2,y2;..."
- * @param mowingWidthStr 鍓茶崏瀹藉害瀛楃涓诧紝濡� "0.34"
- * @param safetyMarginStr 瀹夊叏杈硅窛瀛楃涓诧紝濡� "0.2"
- * @return 璺緞娈靛垪琛�
+ * 瀵瑰涓绘帴鍙�
*/
public static List<PathSegment> planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) {
- // 1. 瑙f瀽鍙傛暟 (浼樺寲锛氬崟鐙彁鍙栬В鏋愰�昏緫)
List<Point> originalPolygon = parseCoords(boundaryCoordsStr);
- double mowingWidth;
- double safetyMargin;
+ double width = Double.parseDouble(mowingWidthStr);
+ double margin = Double.parseDouble(safetyMarginStr);
- try {
- mowingWidth = Double.parseDouble(mowingWidthStr);
- safetyMargin = Double.parseDouble(safetyMarginStr);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("鍓茶崏瀹藉害鎴栧畨鍏ㄨ竟璺濇牸寮忛敊璇�");
+ return planPathCore(originalPolygon, width, margin);
+ }
+
+ private static List<PathSegment> planPathCore(List<Point> originalPolygon, double width, double margin) {
+ if (originalPolygon.size() < 3) return new ArrayList<>();
+
+ // 1. 纭繚閫嗘椂閽堝苟杩涜瀹夊叏鍐呯缉
+ ensureCCW(originalPolygon);
+ List<Point> workArea = shrinkPolygon(originalPolygon, margin);
+ if (workArea.size() < 3) return new ArrayList<>();
+
+ // 2. 棰勮绠楁渶浼樿搴﹀拰濉厖璺緞鐨勭涓�涓偣
+ double bestAngle = findOptimalScanAngle(workArea);
+ Point firstScanStart = getFirstScanStartPoint(workArea, bestAngle, width);
+
+ // 3. 瀵归綈鍥磋竟璧风偣锛氳鍥磋竟鐨勬渶鍚庝竴涓偣鍒氬ソ杩炴帴鎵弿濉厖鐨勮捣鐐�
+ List<Point> alignedWorkArea = alignBoundaryToStart(workArea, firstScanStart);
+
+ List<PathSegment> finalPath = new ArrayList<>();
+
+ // 4. 銆愮涓�闃舵銆戞坊鍔犲洿杈瑰潗鏍囪矾寰�
+ for (int i = 0; i < alignedWorkArea.size(); i++) {
+ Point p1 = alignedWorkArea.get(i);
+ Point p2 = alignedWorkArea.get((i + 1) % alignedWorkArea.size());
+ finalPath.add(new PathSegment(p1, p2, true));
}
- // 2. 璋冪敤鏍稿績绠楁硶
- return planPathCore(originalPolygon, mowingWidth, safetyMargin);
+ // 5. 銆愮浜岄樁娈点�戠敓鎴愬唴閮ㄥ紦瀛楀舰璺緞
+ // 浠庡洿杈归棴鍚堢偣锛坅lignedWorkArea.get(0)锛夊紑濮嬭繛鎺�
+ Point currentPos = alignedWorkArea.get(0);
+ List<PathSegment> zigZagLines = generateZigZagPath(workArea, bestAngle, width, currentPos);
+
+ finalPath.addAll(zigZagLines);
+
+ return finalPath;
}
/**
- * 鏍稿績绠楁硶閫昏緫 (寮虹被鍨嬪叆鍙傦紝渚夸簬娴嬭瘯鍜屽唴閮ㄨ皟鐢�)
+ * 瀵绘壘寮撳瓧褰㈢殑绗竴鏉$嚎鐨勮捣鐐�
*/
- private static List<PathSegment> planPathCore(List<Point> originalPolygon, double mowingWidth, double safetyMargin) {
- // 浼樺寲锛氫笁瑙掑舰涔熸槸鍚堟硶鐨勫嚫澶氳竟褰紝闄愬埗鏀逛负灏忎簬3
- if (originalPolygon == null || originalPolygon.size() < 3) {
- throw new IllegalArgumentException("澶氳竟褰㈠潗鏍囩偣涓嶈冻3涓紝鏃犳硶鏋勬垚鏈夋晥鍖哄煙");
- }
-
- // 纭繚澶氳竟褰㈡槸閫嗘椂閽堟柟鍚�
- ensureCCW(originalPolygon);
-
- // 1. 鏍规嵁瀹夊叏杈硅窛鍐呯缉
- List<Point> shrunkPolygon = shrinkPolygon(originalPolygon, safetyMargin);
-
- // 浼樺寲锛氬唴缂╁悗濡傛灉澶氳竟褰㈠け鏁堬紙渚嬪鍦板潡澶獎锛屽唴缂╁悗娑堝け锛夛紝鐩存帴杩斿洖绌鸿矾寰勶紝閬垮厤鍚庣画鎶ラ敊
- if (shrunkPolygon.size() < 3) {
- // 杩欓噷鍙互璁板綍鏃ュ織锛氬湴鍧楄繃灏忥紝鏃犳硶婊¤冻瀹夊叏璺濈浣滀笟
- return new ArrayList<>();
- }
-
- // 2. 璁$畻鏈�闀胯竟瑙掑害 (浣滀负鎵弿鏂瑰悜)
- double angle = calculateLongestEdgeAngle(originalPolygon);
-
- // 3. & 4. 鏃嬭浆鎵弿骞跺壀瑁�
- List<PathSegment> mowingLines = generateClippedMowingLines(shrunkPolygon, angle, mowingWidth);
-
- // 5. 寮撳瓧褰㈣繛鎺�
- return connectPathSegments(mowingLines);
- }
-
- // ================= 鏍稿績绠楁硶杈呭姪鏂规硶 =================
-
- private static List<Point> shrinkPolygon(List<Point> polygon, double margin) {
- List<Point> newPoints = new ArrayList<>();
- int n = polygon.size();
-
- for (int i = 0; i < n; i++) {
- Point p1 = polygon.get(i);
- Point p2 = polygon.get((i + 1) % n);
- Point p0 = polygon.get((i - 1 + n) % n);
-
- Line line1 = offsetLine(p1, p2, margin);
- Line line0 = offsetLine(p0, p1, margin);
-
- Point intersection = getIntersection(line0, line1);
-
- // 浼樺寲锛氬鍔犻潪绌哄垽鏂紝涓斿鏋滀氦鐐瑰紓甯歌繙锛堝皷瑙掓晥搴旓級锛屽疄闄呭伐绋嬩腑鍙兘闇�瑕佸垏瑙掑鐞�
- // 杩欓噷鏆備繚鐣欏熀纭�閫昏緫锛屼絾鍦ㄥ嚫澶氳竟褰腑閫氬父娌¢棶棰�
- if (intersection != null) {
- newPoints.add(intersection);
- }
- }
- return newPoints;
- }
-
- private static double calculateLongestEdgeAngle(List<Point> polygon) {
- double maxDistSq = -1;
- double angle = 0;
- int n = polygon.size();
-
- for (int i = 0; i < n; i++) {
- Point p1 = polygon.get(i);
- Point p2 = polygon.get((i + 1) % n);
- double dx = p2.x - p1.x;
- double dy = p2.y - p1.y;
- double distSq = dx * dx + dy * dy;
-
- if (distSq > maxDistSq) {
- maxDistSq = distSq;
- angle = Math.atan2(dy, dx);
- }
- }
- return angle;
- }
-
- private static List<PathSegment> generateClippedMowingLines(List<Point> polygon, double angle, double width) {
- List<PathSegment> segments = new ArrayList<>();
-
- // 鏃嬭浆鑷虫按骞�
- List<Point> rotatedPoly = rotatePolygon(polygon, -angle);
-
+ private static Point getFirstScanStartPoint(List<Point> polygon, double angle, double width) {
+ List<Point> rotated = rotatePolygon(polygon, -angle);
double minY = Double.MAX_VALUE;
- double maxY = -Double.MAX_VALUE;
- for (Point p : rotatedPoly) {
- if (p.y < minY) minY = p.y;
- if (p.y > maxY) maxY = p.y;
- }
+ for (Point p : rotated) minY = Math.min(minY, p.y);
- // 浼樺寲锛氳捣濮嬫壂鎻忕嚎澧炲姞涓�涓井灏忕殑鍋忕Щ閲� EPSILON
- // 閬垮厤鎵弿绾挎濂借惤鍦ㄩ《鐐逛笂锛屽鑷翠氦鐐瑰垽鏂�昏緫鍑虹幇浜屼箟鎬�
- double currentY = minY + width / 2.0 + EPSILON;
-
- while (currentY < maxY) {
- List<Double> xIntersections = new ArrayList<>();
- int n = rotatedPoly.size();
-
- for (int i = 0; i < n; i++) {
- Point p1 = rotatedPoly.get(i);
- Point p2 = rotatedPoly.get((i + 1) % n);
-
- // 浼樺寲锛氬拷鐣ユ按骞崇嚎娈� (p1.y == p2.y)锛岄伩鍏嶉櫎闆堕敊璇紝姘村钩绾挎涓嶅弬涓庡瀭鐩存壂鎻忕嚎姹備氦
- if (Math.abs(p1.y - p2.y) < EPSILON) continue;
-
- // 鍒ゆ柇绾挎鏄惁璺ㄨ秺 currentY
- // 浣跨敤涓ユ牸鐨勪笉绛夊紡閫昏緫閰嶅悎鑼冨洿鍒ゆ柇
- double minP = Math.min(p1.y, p2.y);
- double maxP = Math.max(p1.y, p2.y);
-
- if (currentY >= minP && currentY < maxP) {
- // 绾挎�ф彃鍊兼眰X
- double x = p1.x + (currentY - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
- xIntersections.add(x);
- }
- }
-
- Collections.sort(xIntersections);
-
- // 鎻愬彇绾挎锛岄�氬父鏄垚瀵瑰嚭鐜�
- if (xIntersections.size() >= 2) {
- // 鍙栨渶宸﹀拰鏈�鍙崇偣杩炴帴锛堝簲瀵瑰彲鑳藉嚭鐜扮殑寰皬璁$畻璇樊瀵艰嚧鐨勫涓氦鐐癸級
- double xStart = xIntersections.get(0);
- double xEnd = xIntersections.get(xIntersections.size() - 1);
-
- // 鍙湁褰撶嚎娈甸暱搴﹀ぇ浜庢瀬灏忓�兼椂鎵嶆坊鍔狅紝閬垮厤鐢熸垚鍣偣璺緞
- if (xEnd - xStart > EPSILON) {
- Point rStart = rotatePoint(new Point(xStart, currentY), angle); // 杩欓噷鐨刢urrentY瀹為檯涓婂甫浜唀psilon锛岃繕鍘熸椂娌¢棶棰�
- Point rEnd = rotatePoint(new Point(xEnd, currentY), angle);
- segments.add(new PathSegment(rStart, rEnd, true));
- }
- }
-
- currentY += width;
- }
-
- return segments;
+ double startY = minY + width + EPSILON;
+ List<Double> xIntersections = getXIntersections(rotated, startY);
+ if (xIntersections.isEmpty()) return polygon.get(0);
+
+ Collections.sort(xIntersections);
+ return rotatePoint(new Point(xIntersections.get(0), startY), angle);
}
- private static List<PathSegment> connectPathSegments(List<PathSegment> lines) {
+ /**
+ * 閲嶇粍澶氳竟褰㈤《鐐癸紝浣垮緱绱㈠紩0鐨勭偣鏈�闈犺繎濉厖璧风偣
+ */
+ private static List<Point> alignBoundaryToStart(List<Point> polygon, Point target) {
+ int bestIdx = 0;
+ double minDist = Double.MAX_VALUE;
+ for (int i = 0; i < polygon.size(); i++) {
+ double d = Math.hypot(polygon.get(i).x - target.x, polygon.get(i).y - target.y);
+ if (d < minDist) {
+ minDist = d;
+ bestIdx = i;
+ }
+ }
+ List<Point> aligned = new ArrayList<>();
+ for (int i = 0; i < polygon.size(); i++) {
+ aligned.add(polygon.get((bestIdx + i) % polygon.size()));
+ }
+ return aligned;
+ }
+
+ private static List<PathSegment> generateZigZagPath(List<Point> polygon, double angle, double width, Point startPoint) {
List<PathSegment> result = new ArrayList<>();
- if (lines.isEmpty()) return result;
+ List<Point> rotated = rotatePolygon(polygon, -angle);
- for (int i = 0; i < lines.size(); i++) {
- PathSegment currentLine = lines.get(i);
- Point actualStart, actualEnd;
+ double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE;
+ for (Point p : rotated) {
+ minY = Math.min(minY, p.y);
+ maxY = Math.max(maxY, p.y);
+ }
- // 寮撳瓧褰㈣鍒掞細鍋舵暟琛屾鍚戯紝濂囨暟琛屽弽鍚�
- if (i % 2 == 0) {
- actualStart = currentLine.start;
- actualEnd = currentLine.end;
- } else {
- actualStart = currentLine.end;
- actualEnd = currentLine.start;
+ Point currentPos = startPoint;
+ boolean leftToRight = true;
+ // 璧风偣浠� minY + width 寮�濮嬶紝鍥犱负杈圭紭宸茬粡鍥磋竟鍓茶繃
+ for (double y = minY + width; y < maxY - width / 2; y += width) {
+ List<Double> xInt = getXIntersections(rotated, y);
+ if (xInt.size() < 2) continue;
+ Collections.sort(xInt);
+
+ double xStart = leftToRight ? xInt.get(0) : xInt.get(xInt.size() - 1);
+ double xEnd = leftToRight ? xInt.get(xInt.size() - 1) : xInt.get(0);
+
+ Point pS = rotatePoint(new Point(xStart, y), angle);
+ Point pE = rotatePoint(new Point(xEnd, y), angle);
+
+ // 娣诲姞杩囨浮娈� (濡傛灉鏄粠鍥磋竟鍒囨崲杩囨潵鎴栬�呮崲琛�)
+ if (Math.hypot(currentPos.x - pS.x, currentPos.y - pS.y) > 0.05) {
+ result.add(new PathSegment(currentPos, pS, false));
}
-
- // 娣诲姞杩囨浮娈�
- if (i > 0) {
- Point prevEnd = result.get(result.size() - 1).end;
- // 鍙湁褰撹窛绂荤‘瀹炲瓨鍦ㄦ椂鎵嶆坊鍔犺繃娓℃锛堥伩鍏嶉噸鍚堢偣锛�
- if (distanceSq(prevEnd, actualStart) > EPSILON) {
- result.add(new PathSegment(prevEnd, actualStart, false));
- }
- }
-
- result.add(new PathSegment(actualStart, actualEnd, true));
+ result.add(new PathSegment(pS, pE, true));
+ currentPos = pE;
+ leftToRight = !leftToRight;
}
return result;
}
- // ================= 鍩虹鍑犱綍宸ュ叿 (浼樺寲鐗�) =================
-
- private static List<Point> parseCoords(String s) {
- List<Point> list = new ArrayList<>();
- if (s == null || s.trim().isEmpty()) return list;
-
- String[] parts = s.split(";");
- for (String part : parts) {
- String[] xy = part.split(",");
- if (xy.length >= 2) {
- try {
- double x = Double.parseDouble(xy[0].trim());
- double y = Double.parseDouble(xy[1].trim());
- list.add(new Point(x, y));
- } catch (NumberFormatException e) {
- // 蹇界暐鏍煎紡閿欒鐨勫崟涓偣
- }
+ private static List<Double> getXIntersections(List<Point> rotatedPoly, double y) {
+ List<Double> xIntersections = new ArrayList<>();
+ int n = rotatedPoly.size();
+ for (int i = 0; i < n; i++) {
+ Point p1 = rotatedPoly.get(i);
+ Point p2 = rotatedPoly.get((i + 1) % n);
+ if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) {
+ double x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
+ xIntersections.add(x);
}
}
- return list;
+ return xIntersections;
}
- // Shoelace鍏紡鍒ゆ柇鏂瑰悜骞惰皟鏁�
- private static void ensureCCW(List<Point> polygon) {
- double sum = 0;
+ // --- 鍑犱綍鍩虹宸ュ叿 ---
+
+ private static List<Point> shrinkPolygon(List<Point> polygon, double margin) {
+ List<Point> result = new ArrayList<>();
+ int n = polygon.size();
+ for (int i = 0; i < n; i++) {
+ Point pPrev = polygon.get((i - 1 + n) % n);
+ Point pCurr = polygon.get(i);
+ Point pNext = polygon.get((i + 1) % n);
+
+ double d1x = pCurr.x - pPrev.x, d1y = pCurr.y - pPrev.y;
+ double l1 = Math.hypot(d1x, d1y);
+ double d2x = pNext.x - pCurr.x, d2y = pNext.y - pCurr.y;
+ double l2 = Math.hypot(d2x, d2y);
+
+ double n1x = -d1y / l1, n1y = d1x / l1;
+ double n2x = -d2y / l2, n2y = d2x / l2;
+
+ double bx = n1x + n2x, by = n1y + n2y;
+ double bLen = Math.hypot(bx, by);
+ if (bLen < EPSILON) { bx = n1x; by = n1y; }
+ else { bx /= bLen; by /= bLen; }
+
+ double cosHalf = n1x * bx + n1y * by;
+ double d = margin / Math.max(cosHalf, 0.1);
+ result.add(new Point(pCurr.x + bx * d, pCurr.y + by * d));
+ }
+ return result;
+ }
+
+ private static double findOptimalScanAngle(List<Point> polygon) {
+ double minH = Double.MAX_VALUE;
+ double bestA = 0;
for (int i = 0; i < polygon.size(); i++) {
- Point p1 = polygon.get(i);
- Point p2 = polygon.get((i + 1) % polygon.size());
- sum += (p2.x - p1.x) * (p2.y + p1.y);
+ Point p1 = polygon.get(i), p2 = polygon.get((i + 1) % polygon.size());
+ double angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
+ double h = calculatePolygonHeightAtAngle(polygon, angle);
+ if (h < minH) { minH = h; bestA = angle; }
}
- // 鍋囪鏍囧噯绗涘崱灏斿潗鏍囩郴锛宻um > 0 涓洪『鏃堕拡锛岄渶瑕佸弽杞负閫嗘椂閽�
- if (sum > 0) {
- Collections.reverse(polygon);
+ return bestA;
+ }
+
+ private static double calculatePolygonHeightAtAngle(List<Point> poly, double angle) {
+ double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE;
+ double sin = Math.sin(-angle), cos = Math.cos(-angle);
+ for (Point p : poly) {
+ double ry = p.x * sin + p.y * cos;
+ minY = Math.min(minY, ry); maxY = Math.max(maxY, ry);
}
- }
-
- private static class Line {
- double a, b, c;
- public Line(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
- }
-
- private static Line offsetLine(Point p1, Point p2, double dist) {
- double dx = p2.x - p1.x;
- double dy = p2.y - p1.y;
- double len = Math.sqrt(dx * dx + dy * dy);
-
- // 闃叉闄や互0
- if (len < EPSILON) return new Line(0, 0, 0);
-
- double nx = -dy / len;
- double ny = dx / len;
-
- double newX = p1.x + nx * dist;
- double newY = p1.y + ny * dist;
-
- double a = -dy;
- double b = dx;
- double c = -a * newX - b * newY;
- return new Line(a, b, c);
- }
-
- private static Point getIntersection(Line l1, Line l2) {
- double det = l1.a * l2.b - l2.a * l1.b;
- if (Math.abs(det) < EPSILON) return null; // 骞宠
- double x = (l1.b * l2.c - l2.b * l1.c) / det;
- double y = (l2.a * l1.c - l1.a * l2.c) / det;
- return new Point(x, y);
+ return maxY - minY;
}
private static Point rotatePoint(Point p, double angle) {
- double cos = Math.cos(angle);
- double sin = Math.sin(angle);
- return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos);
+ double c = Math.cos(angle), s = Math.sin(angle);
+ return new Point(p.x * c - p.y * s, p.x * s + p.y * c);
}
private static List<Point> rotatePolygon(List<Point> poly, double angle) {
List<Point> res = new ArrayList<>();
- for (Point p : poly) {
- res.add(rotatePoint(p, angle));
- }
+ for (Point p : poly) res.add(rotatePoint(p, angle));
return res;
}
-
- private static double distanceSq(Point p1, Point p2) {
- return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
+
+ private static void ensureCCW(List<Point> poly) {
+ double s = 0;
+ for (int i = 0; i < poly.size(); i++) {
+ Point p1 = poly.get(i), p2 = poly.get((i + 1) % poly.size());
+ s += (p2.x - p1.x) * (p2.y + p1.y);
+ }
+ if (s > 0) Collections.reverse(poly);
+ }
+
+ private static List<Point> parseCoords(String s) {
+ List<Point> list = new ArrayList<>();
+ for (String p : s.split(";")) {
+ String[] xy = p.split(",");
+ if (xy.length >= 2) list.add(new Point(Double.parseDouble(xy[0]), Double.parseDouble(xy[1])));
+ }
+ return list;
}
}
\ No newline at end of file
--
Gitblit v1.10.0