From 77337135d875cf3ee046e7ca2e3992924d3b6e79 Mon Sep 17 00:00:00 2001
From: ‘liusuyi’ <1951119284@qq.com>
Date: 星期二, 22 八月 2023 09:40:52 +0800
Subject: [PATCH] 一键调度增加按多边形获取人车相机

---
 ard-work/src/main/java/com/ruoyi/device/camera/service/IArdCamerasService.java            |    8 +
 ard-work/src/main/java/com/ruoyi/app/position/service/IArdAppPositionService.java         |    7 +
 ard-work/src/main/java/com/ruoyi/utils/tools/GisTool.java                                 |  109 ++++++++++++++++++
 ard-work/src/main/java/com/ruoyi/scheduling/controller/SchedulingController.java          |   14 ++
 ard-work/src/main/java/com/ruoyi/scheduling/domian/SchedulingParam.java                   |    5 
 ard-work/src/main/java/com/ruoyi/app/position/service/impl/ArdAppPositionServiceImpl.java |   44 +++++++
 ard-work/src/main/java/com/ruoyi/sy/service/IArdSyCarService.java                         |    4 
 ard-work/src/main/java/com/ruoyi/device/camera/service/impl/ArdCamerasServiceImpl.java    |   54 ++++++++
 ard-work/src/main/java/com/ruoyi/sy/service/impl/ArdSyCarServiceImpl.java                 |   66 ++++++++++
 9 files changed, 307 insertions(+), 4 deletions(-)

diff --git a/ard-work/src/main/java/com/ruoyi/app/position/service/IArdAppPositionService.java b/ard-work/src/main/java/com/ruoyi/app/position/service/IArdAppPositionService.java
index 6952476..835646e 100644
--- a/ard-work/src/main/java/com/ruoyi/app/position/service/IArdAppPositionService.java
+++ b/ard-work/src/main/java/com/ruoyi/app/position/service/IArdAppPositionService.java
@@ -73,4 +73,11 @@
      * 2023/8/17 13:56:36
      */
     public List<SysUser>getNearAppUsers(SchedulingParam param);
+
+    /**
+     * 鑾峰彇灏佹帶鍦堝唴鎵�鏈夊湪绾縜pp鐢ㄦ埛(澶氳竟褰�)
+     * 鍒樿嫃涔�
+     * 2023/8/17 13:56:36
+     */
+    public List<SysUser>getNearAppUsersWithPolygon(SchedulingParam param);
 }
diff --git a/ard-work/src/main/java/com/ruoyi/app/position/service/impl/ArdAppPositionServiceImpl.java b/ard-work/src/main/java/com/ruoyi/app/position/service/impl/ArdAppPositionServiceImpl.java
index c046a48..905531f 100644
--- a/ard-work/src/main/java/com/ruoyi/app/position/service/impl/ArdAppPositionServiceImpl.java
+++ b/ard-work/src/main/java/com/ruoyi/app/position/service/impl/ArdAppPositionServiceImpl.java
@@ -18,6 +18,7 @@
 
 import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
+import java.awt.geom.Point2D;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -172,5 +173,48 @@
         }
         return filteredList;
     }
+    /**
+     * 鑾峰彇灏佹帶鍦堝唴鎵�鏈夊湪绾縜pp鐢ㄦ埛(澶氳竟褰�)
+     * 鍒樿嫃涔�
+     * 2023/8/17 13:56:36
+     */
+    @Override
+    public List<SysUser> getNearAppUsersWithPolygon(SchedulingParam param) {
+        List<SysUser> filteredList = new ArrayList<>();
+        try {
+            Long deptId = SecurityUtils.getLoginUser().getUser().getDeptId();
+            List<Point2D> partitionLocation = param.getPartitionLocation();
 
+            SysUser user = new SysUser();
+            user.setDeptId(deptId);
+            List<SysUser> appUserList = iSysUserService.selectAllAppUserList(user);
+            //杩囨护鍦ㄧ嚎
+            List<SysUser> onLineList = appUserList.stream()
+                    .filter(sysUser -> (sysUser.getAppOnlineState().equals("1")))
+                    .collect(Collectors.toList());
+            //杩囨护鑼冨洿
+            for (SysUser sysUser : onLineList) {
+                ArdAppPosition ardAppPosition = ardAppPositionMapper.selectLastArdAppPositionByUserId(sysUser.getUserId());
+                if (ardAppPosition != null) {
+                    Double lon = ardAppPosition.getLongitude();
+                    Double lat = ardAppPosition.getLatitude();
+                    if (lon == null || lat == null) {
+                        continue;
+                    }
+                    Point2D point2D=new Point2D.Double(lon,lat);
+                    boolean inPolygon = GisTool.isInPolygon(point2D, partitionLocation);
+                    if (inPolygon) {
+                        Map<String, Object> params = new HashMap<>();
+                        params.put("longitude", lon);
+                        params.put("latitude", lat);
+                        sysUser.setParams(params);
+                        filteredList.add(sysUser); // 灏嗘弧瓒虫潯浠剁殑鐢ㄦ埛娣诲姞鍒扮瓫閫夊垪琛ㄤ腑
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("鑾峰彇灏佹帶鍦堝唴鎵�鏈夊湪绾縜pp鐢ㄦ埛寮傚父" + ex.getMessage());
+        }
+        return filteredList;
+    }
 }
diff --git a/ard-work/src/main/java/com/ruoyi/device/camera/service/IArdCamerasService.java b/ard-work/src/main/java/com/ruoyi/device/camera/service/IArdCamerasService.java
index 4236c78..c51ff05 100644
--- a/ard-work/src/main/java/com/ruoyi/device/camera/service/IArdCamerasService.java
+++ b/ard-work/src/main/java/com/ruoyi/device/camera/service/IArdCamerasService.java
@@ -86,9 +86,15 @@
     public TreeMap getNearCamerasBycoordinate(CameraCmd cmd);
 
     /**
-     * 鑾峰彇鐩戞帶鍦堝唴鎵�鏈夊湪绾垮厜鐢�
+     * 鑾峰彇鐩戞帶鍦堝唴鎵�鏈夊湪绾垮厜鐢�(鍗婂緞)
      * 鍒樿嫃涔�
      * 2023/8/17 13:56:36
      */
     List<ArdCameras> getNearCameras(SchedulingParam param);
+    /**
+     * 鑾峰彇鐩戞帶鍦堝唴鎵�鏈夊湪绾垮厜鐢�(澶氳竟褰�)
+     * 鍒樿嫃涔�
+     * 2023/8/17 13:56:36
+     */
+    public List<ArdCameras> getNearCamerasWithPolygon(SchedulingParam param);
 }
diff --git a/ard-work/src/main/java/com/ruoyi/device/camera/service/impl/ArdCamerasServiceImpl.java b/ard-work/src/main/java/com/ruoyi/device/camera/service/impl/ArdCamerasServiceImpl.java
index e14f950..6792122 100644
--- a/ard-work/src/main/java/com/ruoyi/device/camera/service/impl/ArdCamerasServiceImpl.java
+++ b/ard-work/src/main/java/com/ruoyi/device/camera/service/impl/ArdCamerasServiceImpl.java
@@ -1,5 +1,6 @@
 package com.ruoyi.device.camera.service.impl;
 
+import java.awt.geom.Point2D;
 import java.time.LocalTime;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -329,7 +330,58 @@
                 double[] camPosition = new double[]{camera.getLongitude(), camera.getLatitude()};
                 double distance = GisTool.getDistance(new double[]{longitude, latitude}, camPosition);
                 if (distance <= radius) {
-                    camera.setChanNo(ArdTool.getChannelBydayNightTime(dayNightTime));
+                    /*鑾峰彇閫氶亾鍒楄〃*/
+                    ArdChannel ardChannel=new ArdChannel();
+                    ardChannel.setDeviceId(camera.getId());
+                    List<ArdChannel> ardChannels = ardChannelMapper.selectArdChannelList(ardChannel);
+                    camera.setChannelList(ardChannels);
+                    ardCameras.add(camera);
+                }
+            }
+            //杩囨护鍦ㄧ嚎鐩告満
+            List<ArdCameras> onlineList = ardCameras.stream()
+                    .filter(ardCamera -> (!ardCamera.getLoginId().equals(-1)))
+                    .collect(Collectors.toList());
+            return onlineList;
+        } catch (Exception ex) {
+            log.error("鑾峰彇闄勮繎鐩告満寮傚父:" + ex.getMessage());
+        }
+        return null;
+    }
+    /**
+     * 鑾峰彇鐩戞帶鍦堝唴鎵�鏈夊湪绾垮厜鐢�
+     * 鍒樿嫃涔�
+     * 2023/8/17 13:57:21
+     */
+    @Override
+    public List<ArdCameras> getNearCamerasWithPolygon(SchedulingParam param) {
+        try {
+            Long deptId=SecurityUtils.getLoginUser().getUser().getDeptId();
+            List<Point2D> partitionLocation = param.getPartitionLocation();
+            if(partitionLocation==null)
+            {
+                log.debug("澶氳竟褰㈠潗鏍囬泦鍚堜负绌�");
+                return null;
+            }
+            String dayNightTime = redisCache.getCacheObject("sys_config:dayNightTime");
+            //鑾峰彇鎵�鏈夊厜鐢�(鎸夐儴闂�)
+            ArdCameras cameras= new ArdCameras();
+            cameras.setDeptId(deptId);
+            List<ArdCameras> ardCamerasList = ardCamerasMapper.selectArdCamerasList(cameras);
+            List<ArdCameras> ardCameras = new ArrayList<>();
+            for (ArdCameras camera : ardCamerasList) {
+                if (camera.getLongitude() == null && camera.getLatitude() == null) {
+                    continue;
+                }
+                /*鍒ゆ柇鍧愭爣鏄惁鍦ㄥ杈瑰舰鑼冨洿鍐�*/
+                Point2D camPosition=new Point2D.Double(camera.getLongitude(), camera.getLatitude());
+                boolean inPolygon = GisTool.isInPolygon(camPosition, partitionLocation);
+                if (inPolygon) {
+                    /*鑾峰彇閫氶亾鍒楄〃*/
+                    ArdChannel ardChannel=new ArdChannel();
+                    ardChannel.setDeviceId(camera.getId());
+                    List<ArdChannel> ardChannels = ardChannelMapper.selectArdChannelList(ardChannel);
+                    camera.setChannelList(ardChannels);
                     ardCameras.add(camera);
                 }
             }
diff --git a/ard-work/src/main/java/com/ruoyi/scheduling/controller/SchedulingController.java b/ard-work/src/main/java/com/ruoyi/scheduling/controller/SchedulingController.java
index 63029af..5a75f3b 100644
--- a/ard-work/src/main/java/com/ruoyi/scheduling/controller/SchedulingController.java
+++ b/ard-work/src/main/java/com/ruoyi/scheduling/controller/SchedulingController.java
@@ -38,7 +38,7 @@
     IArdAppPositionService iArdAppPositionService;
 
     @GetMapping("/getNearVehiPersonCam")
-    @ApiOperation("鑾峰彇闄勮繎鐨勮溅浜虹浉鏈�")
+    @ApiOperation("鑾峰彇闄勮繎鐨勮溅浜虹浉鏈�(鍗婂緞)")
     AjaxResult getNearVehiPersonCam(SchedulingParam param) {
         Map<String, Object> nearMap = new HashMap<>();
 
@@ -51,4 +51,16 @@
 
         return AjaxResult.success(nearMap);
     }
+    @GetMapping("/getPolygonVehiPersonCam")
+    @ApiOperation("鑾峰彇闄勮繎鐨勮溅浜虹浉鏈�(澶氳竟褰�)")
+    AjaxResult getPolygonVehiPersonCam(SchedulingParam param) {
+        Map<String, Object> nearMap = new HashMap<>();
+        List<Map<String, Object>> Cars = iArdSyCarService.getNearCarWithPolygon(param);
+        nearMap.put("car", Cars);
+        List<ArdCameras> Cameras = iArdCamerasService.getNearCamerasWithPolygon(param);
+        nearMap.put("camera", Cameras);
+        List<SysUser> AppUsers = iArdAppPositionService.getNearAppUsersWithPolygon(param);
+        nearMap.put("users", AppUsers);
+        return AjaxResult.success(nearMap);
+    }
 }
diff --git a/ard-work/src/main/java/com/ruoyi/scheduling/domian/SchedulingParam.java b/ard-work/src/main/java/com/ruoyi/scheduling/domian/SchedulingParam.java
index b75ed90..0c85e9d 100644
--- a/ard-work/src/main/java/com/ruoyi/scheduling/domian/SchedulingParam.java
+++ b/ard-work/src/main/java/com/ruoyi/scheduling/domian/SchedulingParam.java
@@ -2,6 +2,9 @@
 
 import lombok.Data;
 
+import java.awt.geom.Point2D;
+import java.util.List;
+
 /**
  * @Description:
  * @ClassName: RequestParam
@@ -16,4 +19,6 @@
     Integer monitoringRadius;
     Double longitude;
     Double latitude;
+    //澶氳竟褰㈠潗鏍囬泦鍚�
+    List<Point2D> partitionLocation;
 }
diff --git a/ard-work/src/main/java/com/ruoyi/sy/service/IArdSyCarService.java b/ard-work/src/main/java/com/ruoyi/sy/service/IArdSyCarService.java
index 4b7a002..e2d1987 100644
--- a/ard-work/src/main/java/com/ruoyi/sy/service/IArdSyCarService.java
+++ b/ard-work/src/main/java/com/ruoyi/sy/service/IArdSyCarService.java
@@ -139,4 +139,8 @@
      * 鑾峰彇闄勮繎鑼冨洿鍐呯殑杞﹁締淇℃伅
      * */
     List<Map<String, Object>> getNearCar(SchedulingParam param);
+    /**
+     * 鑾峰彇闄勮繎鑼冨洿鍐呯殑杞﹁締淇℃伅(澶氳竟褰�)
+     * */
+    List<Map<String, Object>> getNearCarWithPolygon(SchedulingParam param);
 }
diff --git a/ard-work/src/main/java/com/ruoyi/sy/service/impl/ArdSyCarServiceImpl.java b/ard-work/src/main/java/com/ruoyi/sy/service/impl/ArdSyCarServiceImpl.java
index bf19a74..cb494e3 100644
--- a/ard-work/src/main/java/com/ruoyi/sy/service/impl/ArdSyCarServiceImpl.java
+++ b/ard-work/src/main/java/com/ruoyi/sy/service/impl/ArdSyCarServiceImpl.java
@@ -1,5 +1,6 @@
 package com.ruoyi.sy.service.impl;
 
+import java.awt.geom.Point2D;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.ParseException;
@@ -1032,7 +1033,6 @@
     /**
      * 鑾峰彇闄勮繎鑼冨洿鍐呯殑杞﹁締淇℃伅
      */
-
     @Override
     public List<Map<String, Object>> getNearCar(SchedulingParam param) {
         Double longitude = param.getLongitude();
@@ -1092,4 +1092,68 @@
         }
         return filteredList;
     }
+    /**
+     * 鑾峰彇鑼冨洿鍐呯殑杞﹁締淇℃伅(澶氳竟褰�)
+     */
+    @Override
+    public List<Map<String, Object>> getNearCarWithPolygon(SchedulingParam param) {
+        List<Map<String, Object>> filteredList = new ArrayList<>();
+        try {
+            List<Point2D> partitionLocation = param.getPartitionLocation();
+            if (partitionLocation == null) {
+                log.debug("澶氳竟褰㈠潗鏍囬泦鍚堜负绌�");
+                return null;
+            }
+            String userId = SecurityUtils.getUserId();
+            ArdSyUser syUser = new ArdSyUser();
+            syUser.setSysUserId(userId);
+            List<ArdSyUser> ardSyUserList = ardSyUserMapper.selectArdSyUserList(syUser);
+            if (ardSyUserList.size() == 0) {
+                log.debug("鐢ㄦ埛鏈寕鎺ヤ笁涓�杞﹁締");
+                return null;
+            }
+            ArdSyUser ardSyUser = ardSyUserList.get(0);
+            String syUrl = redisCache.getCacheObject("sys_config:syCarPT");
+            String passwordMd5 = DigestUtils.md5Hex(ardSyUser.getPassword());
+            Map<String, Object> LogInResult = sYClient.logIn(syUrl, passwordMd5, ardSyUser.getUserId());
+            String sessionId = (String) LogInResult.get("sessionId");
+
+            Map<String, Object> teamList = sYClient.getTeamList(syUrl, ardSyUser.getUserId(), sessionId);
+            List<Map<String, Object>> listMap = (List<Map<String, Object>>) teamList.get("list");
+            List<Map<String, Object>> allList = new ArrayList<>();
+            for (Map<String, Object> team : listMap) {
+                String teamId = (String) team.get("teamId");
+                Map<String, Object> carListMap = sYClient.getCarList1(syUrl, teamId, ardSyUser.getUserId(), sessionId);
+                if (((String) carListMap.get("rspCode")).equals("1")) {
+                    List<Map<String, Object>> list = (List<Map<String, Object>>) carListMap.get("list");
+                    allList.addAll(list);
+                }
+            }
+            //杩囨护鍦ㄧ嚎杞﹁締
+            List<Map<String, Object>> onlineList = allList.stream()
+                    .filter(map -> !"绂荤嚎".equals(map.get("stateCn")))
+                    .collect(Collectors.toList());
+
+            //杩囨护鍗婂緞
+            for (Map<String, Object> carMap : onlineList) {
+                String carId = (String) carMap.get("carId");
+                Map<String, Object> carGPSTrack = sYClient.getCarNearPositionByCarId(syUrl, carId, ardSyUser.getUserId(), sessionId);
+                List<Map<String, Object>> carGPSMap = (List<Map<String, Object>>) carGPSTrack.get("list");
+                Double lng = Double.valueOf((String) carGPSMap.get(0).get("lng"));
+                Double lat = Double.valueOf((String) carGPSMap.get(0).get("lat"));
+                Point2D point2D = new Point2D.Double(lng, lat);
+                boolean inPolygon = GisTool.isInPolygon(point2D, partitionLocation);
+                if (inPolygon) {
+                    carMap.put("longitude", lng);
+                    carMap.put("latitude", lat);
+                    filteredList.add(carMap); // 灏嗘弧瓒虫潯浠剁殑杞﹁締娣诲姞鍒扮瓫閫夊垪琛ㄤ腑
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            log.error("鑾峰彇鑼冨洿鍐呯殑杞﹁締淇℃伅(澶氳竟褰�)寮傚父:" + ex.getMessage());
+        }
+        return filteredList;
+    }
 }
diff --git a/ard-work/src/main/java/com/ruoyi/utils/tools/GisTool.java b/ard-work/src/main/java/com/ruoyi/utils/tools/GisTool.java
index 661fd91..bb36308 100644
--- a/ard-work/src/main/java/com/ruoyi/utils/tools/GisTool.java
+++ b/ard-work/src/main/java/com/ruoyi/utils/tools/GisTool.java
@@ -4,6 +4,10 @@
 import org.gavaghan.geodesy.GeodeticCalculator;
 import org.gavaghan.geodesy.GlobalCoordinates;
 
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * @Description:
  * @ClassName: GisTool
@@ -29,4 +33,109 @@
         GlobalCoordinates target = new GlobalCoordinates(latitudeTo, longitudeTo);
         return geodeticCalculator.calculateGeodeticCurve(Ellipsoid.WGS84, source, target).getEllipsoidalDistance();
     }
+
+    public static void main(String[] args) {
+        // 琚娴嬬殑缁忕含搴︾偣
+        Point2D point2D= new Point2D.Double(126.649261,45.687377);
+        // 鍟嗕笟鍖哄煙锛堢櫨搴﹀杈瑰舰鍖哄煙缁忕含搴﹂泦鍚堬級
+        List<Point2D> partitionLocation = new ArrayList<>();
+        partitionLocation.add(new Point2D.Double(126.64459,45.688548));
+        partitionLocation.add(new Point2D.Double(126.653376,45.68938));
+        partitionLocation.add(new Point2D.Double(126.645776,45.685048));
+        partitionLocation.add(new Point2D.Double(126.654184,45.685778));
+        System.out.println(isInPolygon(point2D,partitionLocation));
+    }
+
+    /**
+     * 鍒ゆ柇褰撳墠浣嶇疆鏄惁鍦ㄥ杈瑰舰鍖哄煙鍐�
+     * @param orderLocation 褰撳墠鐐�
+     * @param partitionLocation 鍖哄煙椤剁偣
+     * @return
+     */
+    public static boolean isInPolygon(Point2D orderLocation,List<Point2D> partitionLocation){
+
+        double p_x =orderLocation.getX();
+        double p_y =orderLocation.getY();
+        Point2D.Double point = new Point2D.Double(p_x, p_y);
+
+        List<Point2D.Double> pointList= new ArrayList<Point2D.Double>();
+
+        for (Point2D points : partitionLocation){
+            double polygonPoint_x=points.getX();
+            double polygonPoint_y=points.getY();
+            Point2D.Double polygonPoint = new Point2D.Double(polygonPoint_x,polygonPoint_y);
+            pointList.add(polygonPoint);
+        }
+        return IsPtInPoly(point,pointList);
+    }
+    /**
+     * 鍒ゆ柇鐐规槸鍚﹀湪澶氳竟褰㈠唴锛屽鏋滅偣浣嶄簬澶氳竟褰㈢殑椤剁偣鎴栬竟涓婏紝涔熺畻鍋氱偣鍦ㄥ杈瑰舰鍐咃紝鐩存帴杩斿洖true
+     * @param point 妫�娴嬬偣
+     * @param pts   澶氳竟褰㈢殑椤剁偣
+     * @return      鐐瑰湪澶氳竟褰㈠唴杩斿洖true,鍚﹀垯杩斿洖false
+     */
+    public static boolean IsPtInPoly(Point2D.Double point, List<Point2D.Double> pts){
+
+        int N = pts.size();
+        boolean boundOrVertex = true; //濡傛灉鐐逛綅浜庡杈瑰舰鐨勯《鐐规垨杈逛笂锛屼篃绠楀仛鐐瑰湪澶氳竟褰㈠唴锛岀洿鎺ヨ繑鍥瀟rue
+        int intersectCount = 0;//cross points count of x
+        double precision = 2e-10; //娴偣绫诲瀷璁$畻鏃跺�欎笌0姣旇緝鏃跺�欑殑瀹瑰樊
+        Point2D.Double p1, p2;//neighbour bound vertices
+        Point2D.Double p = point; //褰撳墠鐐�
+
+        p1 = pts.get(0);//left vertex
+        for(int i = 1; i <= N; ++i){//check all rays
+            if(p.equals(p1)){
+                return boundOrVertex;//p is an vertex
+            }
+
+            p2 = pts.get(i % N);
+            if(p.x < Math.min(p1.x, p2.x) || p.x > Math.max(p1.x, p2.x)){
+                p1 = p2;
+                continue;
+            }
+
+            if(p.x > Math.min(p1.x, p2.x) && p.x < Math.max(p1.x, p2.x)){
+                if(p.y <= Math.max(p1.y, p2.y)){
+                    if(p1.x == p2.x && p.y >= Math.min(p1.y, p2.y)){
+                        return boundOrVertex;
+                    }
+
+                    if(p1.y == p2.y){
+                        if(p1.y == p.y){
+                            return boundOrVertex;
+                        }else{//before ray
+                            ++intersectCount;
+                        }
+                    }else{
+                        double xinters = (p.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;
+                        if(Math.abs(p.y - xinters) < precision){
+                            return boundOrVertex;
+                        }
+
+                        if(p.y < xinters){
+                            ++intersectCount;
+                        }
+                    }
+                }
+            }else{
+                if(p.x == p2.x && p.y <= p2.y){
+                    Point2D.Double p3 = pts.get((i+1) % N);
+                    if(p.x >= Math.min(p1.x, p3.x) && p.x <= Math.max(p1.x, p3.x)){
+                        ++intersectCount;
+                    }else{
+                        intersectCount += 2;
+                    }
+                }
+            }
+            p1 = p2;
+        }
+
+        if(intersectCount % 2 == 0){//鍋舵暟鍦ㄥ杈瑰舰澶�
+            return false;
+        } else { //濂囨暟鍦ㄥ杈瑰舰鍐�
+            return true;
+        }
+    }
+
 }

--
Gitblit v1.9.3