Browse Source

1.添加WebSocket组件;
2.完善地图页面及数据逻辑;
3.添加若干资源。

Yumin 6 years ago
parent
commit
f0725cd04e

+ 12 - 0
pom.xml

@@ -85,6 +85,18 @@
             <version>1.2.38</version>
         </dependency>
 
+        <!-- WebSocket -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
         <!-- MQTT -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 2 - 0
src/main/java/cn/minbb/iot/config/Const.java

@@ -2,6 +2,8 @@ package cn.minbb.iot.config;
 
 public class Const {
     public static final String MQTT_TOPIC_ALL = "iot.all";
+    public static final String MQTT_TOPIC_TIME = "iot.time";
     public static final String MQTT_TOPIC_CAR = "iot.car";
+    public static final String MQTT_TOPIC_CAR_DATA = "iot.car.data";
     public static final String MQTT_TOPIC_CAR_CTRL = "iot.car.ctrl";
 }

+ 27 - 6
src/main/java/cn/minbb/iot/config/MqttSenderConfig.java

@@ -1,5 +1,9 @@
 package cn.minbb.iot.config;
 
+import cn.minbb.iot.model.DeviceData;
+import cn.minbb.iot.service.WebSocketService;
+import cn.minbb.iot.util.Application;
+import com.alibaba.fastjson.JSONObject;
 import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -88,9 +92,8 @@ public class MqttSenderConfig {
     @Bean
     public MessageProducer inbound() {
         MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
-                clientId + "_inbound", mqttClientFactory(),
-                defaultTopic,
-                Const.MQTT_TOPIC_ALL, Const.MQTT_TOPIC_CAR);
+                clientId + "_inbound", mqttClientFactory(), defaultTopic,
+                Const.MQTT_TOPIC_ALL, Const.MQTT_TOPIC_CAR, Const.MQTT_TOPIC_CAR_DATA);
         adapter.setCompletionTimeout(completionTimeout);
         adapter.setConverter(new DefaultPahoMessageConverter());
         adapter.setQos(1);
@@ -105,9 +108,27 @@ public class MqttSenderConfig {
         return message -> {
             Object object = message.getHeaders().get("mqtt_receivedTopic");
             String topic = null != object ? object.toString() : "";
-            String type = topic.substring(topic.lastIndexOf("/") + 1, topic.length());
-            String order = message.getPayload().toString();
-            logger.info("收到消息 = 主题 = {}  信息 = {}", topic, order);
+            String msg = message.getPayload().toString();
+            logger.info("收到消息 = 主题 = {}  信息 = {}", topic, msg);
+            switch (topic) {
+                case Const.MQTT_TOPIC_CAR_DATA:
+                    try {
+                        DeviceData data = JSONObject.parseObject(msg, DeviceData.class);
+                        String longitudeString = data.getLongitude();
+                        String latitudeString = data.getLatitude();
+                        if (!longitudeString.isEmpty() && !latitudeString.isEmpty()) {
+                            data.setLongitude(String.valueOf(Double.parseDouble(longitudeString.substring(0, 2)) + Double.parseDouble(longitudeString.substring(2, 10)) / 60));
+                            data.setLatitude(String.valueOf(Double.parseDouble(latitudeString.substring(0, 3)) + Double.parseDouble(latitudeString.substring(3, 11)) / 60));
+                        }
+                        WebSocketService.sendInfo(
+                                JSONObject.toJSONString(
+                                        new DeviceData(data.getLongitude(), data.getLatitude(), Application.getCurrentStringTime())));
+                    } catch (Exception e) {
+                        logger.info("非JSON格式字符串 | {}", e.getMessage());
+                    }
+                    break;
+                default:
+            }
         };
     }
 }

+ 13 - 0
src/main/java/cn/minbb/iot/config/WebSocketConfig.java

@@ -0,0 +1,13 @@
+package cn.minbb.iot.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+public class WebSocketConfig {
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 15 - 11
src/main/java/cn/minbb/iot/model/DeviceData.java

@@ -4,7 +4,6 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
 
 import javax.persistence.*;
 import java.io.Serializable;
@@ -39,13 +38,13 @@ public class DeviceData implements Serializable {
 
     @Getter
     @Setter
-    @Column(name = "latitude", columnDefinition = "VARCHAR(16) COMMENT '纬度'")
-    private String latitude;
+    @Column(name = "longitude", columnDefinition = "VARCHAR(16) COMMENT '经度'")
+    private String longitude;
 
     @Getter
     @Setter
-    @Column(name = "longitude", columnDefinition = "VARCHAR(16) COMMENT '经度'")
-    private String longitude;
+    @Column(name = "latitude", columnDefinition = "VARCHAR(16) COMMENT '纬度'")
+    private String latitude;
 
     @Getter
     @Setter
@@ -62,6 +61,11 @@ public class DeviceData implements Serializable {
     @Column(name = "bei_jing_time", columnDefinition = "INTEGER COMMENT '北京时间'")
     private Integer beiJingTime;
 
+    @Getter
+    @Setter
+    @Column(name = "remark", columnDefinition = "VARCHAR(64) COMMENT '备注'")
+    private String remark;
+
     @Getter
     @Setter
     @Column(name = "device_id", nullable = false, columnDefinition = "BIGINT COMMENT '关联设备'")
@@ -73,13 +77,13 @@ public class DeviceData implements Serializable {
     @CreationTimestamp
     private Date createdAt;
 
-    @Getter
-    @Setter
-    @Column(name = "updated_at", columnDefinition = "DATETIME COMMENT '更新时间'")
-    @UpdateTimestamp
-    private Date updatedAt;
-
     @Version
     @Column(name = "version", columnDefinition = "BIGINT COMMENT '版本号'")
     public Long version;
+
+    public DeviceData(String longitude, String latitude, String remark) {
+        this.longitude = longitude;
+        this.latitude = latitude;
+        this.remark = remark;
+    }
 }

+ 99 - 0
src/main/java/cn/minbb/iot/service/WebSocketService.java

@@ -0,0 +1,99 @@
+package cn.minbb.iot.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+@Component
+@ServerEndpoint(value = "/ws/map")
+public class WebSocketService {
+
+    private Logger logger = LoggerFactory.getLogger(WebSocketService.class);
+
+    // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
+    private static int onlineCount = 0;
+    // concurrent 包的线程安全Set,用来存放每个客户端对应的 WebSocket 对象。
+    private static CopyOnWriteArraySet<WebSocketService> webSocketSet = new CopyOnWriteArraySet<>();
+    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
+    private Session session;
+
+    /**
+     * 连接建立成功调用的方法
+     *
+     * @param session
+     */
+    @OnOpen
+    public void onOpen(Session session) {
+        this.session = session;
+        webSocketSet.add(this);
+        addOnlineCount();
+        logger.info("有新连接加入!当前在线数量为 {}", getOnlineCount());
+    }
+
+    /**
+     * 连接关闭调用的方法
+     */
+    @OnClose
+    public void onClose() {
+        webSocketSet.remove(this);
+        subOnlineCount();
+        logger.info("有一连接关闭!当前在线数量为 {}", getOnlineCount());
+    }
+
+    /**
+     * 收到客户端消息后调用的方法
+     *
+     * @param message 客户端发送过来的消息
+     */
+    @OnMessage
+    public void onMessage(String message, Session session) {
+        logger.info("来自客户端的消息 = {}", message);
+        // 群发消息
+        for (WebSocketService item : webSocketSet) {
+            item.sendMessage(message);
+        }
+    }
+
+    /**
+     * 发生错误时调用
+     *
+     * @param session
+     * @param error
+     */
+    @OnError
+    public void onError(Session session, Throwable error) {
+        logger.error("发生错误");
+        error.printStackTrace();
+    }
+
+    private void sendMessage(String message) {
+        this.session.getAsyncRemote().sendText(message);
+    }
+
+    /**
+     * 群发自定义消息
+     *
+     * @param message
+     */
+    public static void sendInfo(String message) {
+        for (WebSocketService item : webSocketSet) {
+            item.sendMessage(message);
+        }
+    }
+
+    public static synchronized int getOnlineCount() {
+        return onlineCount;
+    }
+
+    public static synchronized void addOnlineCount() {
+        WebSocketService.onlineCount++;
+    }
+
+    public static synchronized void subOnlineCount() {
+        WebSocketService.onlineCount--;
+    }
+}

+ 13 - 3
src/main/java/cn/minbb/iot/task/AutoConfig.java

@@ -1,12 +1,17 @@
 package cn.minbb.iot.task;
 
 import cn.minbb.iot.config.Const;
+import cn.minbb.iot.model.DeviceData;
 import cn.minbb.iot.service.MqttGateway;
+import cn.minbb.iot.util.Application;
+import com.alibaba.fastjson.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.Random;
+
 @Component
 public class AutoConfig {
 
@@ -19,11 +24,16 @@ public class AutoConfig {
     }
 
     /**
-     * 从程序启动开始,每间隔 60s 执行一次
+     * 从程序启动开始,每间隔 0s 执行一次
      * 服务端心跳广播,延时 1 秒
      */
-    @Scheduled(fixedRate = 60000, initialDelay = 1000)
+    @Scheduled(fixedRate = 3000, initialDelay = 1000)
     public void scheduled() {
-        mqttGateway.sendToMqtt("*", Const.MQTT_TOPIC_ALL, 2);
+        int random1 = 1 + ((int) (new Random().nextFloat() * (10 - 1)));
+        mqttGateway.sendToMqtt(Application.getCurrentStringTime(), Const.MQTT_TOPIC_TIME, 2);
+        int random2 = 1 + ((int) (new Random().nextFloat() * (10 - 1)));
+        mqttGateway.sendToMqtt(JSONObject.toJSONString(
+                new DeviceData("3447.9" + random1 + "043 N", "11339.2" + random2 + "603 E",
+                        Application.getCurrentStringTime())), Const.MQTT_TOPIC_CAR_DATA, 2);
     }
 }

+ 15 - 0
src/main/java/cn/minbb/iot/util/StringUtil.java

@@ -0,0 +1,15 @@
+package cn.minbb.iot.util;
+
+import com.alibaba.fastjson.JSONObject;
+
+public class StringUtil {
+
+    public static boolean isJson(String content) {
+        try {
+            JSONObject.parseObject(content);
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}

BIN
src/main/resources/static/favicon.ico


BIN
src/main/resources/static/images/car.png


+ 102 - 0
src/main/resources/static/js/map.js

@@ -0,0 +1,102 @@
+function getCity(result) {
+    let cityName = result.name;
+    map.setCenter(cityName);
+    console.log(cityName);
+    return cityName;
+}
+
+// 移动车辆,count两点间要移动的次数,timer,每次移动的时间,毫秒
+function moveCar(prvePoint, newPoint, timer, marker, count) {
+    let _prvePoint = new BMap.Pixel(0, 0);
+    let _newPoint = new BMap.Pixel(0, 0);
+    // 当前帧数
+    let currentCount = 0;
+    // 初始坐标
+    _prvePoint = map.getMapType().getProjection().lngLatToPoint(prvePoint);
+    // 获取结束点的(x,y)坐标
+    _newPoint = map.getMapType().getProjection().lngLatToPoint(newPoint);
+
+    // 两点之间匀速移动
+    let intervalFlag = setInterval(function () {
+        // 两点之间当前帧数大于总帧数的时候,则说明已经完成移动
+        if (currentCount >= count) {
+            clearInterval(intervalFlag);
+            moveMap(newPoint, timer, marker, count);
+        } else {
+            // 动画移动
+            currentCount++;//计数
+            let x = linear(_prvePoint.x, _newPoint.x, currentCount, count);
+            let y = linear(_prvePoint.y, _newPoint.y, currentCount, count);
+            // 根据平面坐标转化为球面坐标
+            let pos = map.getMapType().getProjection().pointToLngLat(new BMap.Pixel(x, y));
+            marker.setPosition(pos);
+            // 移动覆盖物
+            map.clearOverlays();
+            marker.setLabel(label);
+            map.addOverlay(marker);
+            map.addOverlay(new BMap.Circle(pos, 160, {fillColor: "gray", strokeWeight: 1, fillOpacity: 0.3, strokeOpacity: 0.3}));
+            // 调整方向
+            setRotation(prvePoint, newPoint, marker);
+        }
+    }, timer);
+}
+
+function moveMap(newPoint, timer, marker, count) {
+    let center = map.getBounds().getCenter();
+    // 可视区域中心点坐标
+    let centerPoint = new BMap.Point(center.lng, center.lat);
+    let _centerPoint = new BMap.Pixel(0, 0);
+    let _newPoint = new BMap.Pixel(0, 0);
+    // 当前帧数
+    let currentCount = 0;
+    // 初始坐标
+    _centerPoint = map.getMapType().getProjection().lngLatToPoint(centerPoint);
+    // 获取结束点的(x,y)坐标
+    _newPoint = map.getMapType().getProjection().lngLatToPoint(newPoint);
+    // 两点之间匀速移动
+    let intervalFlag = setInterval(function () {
+        // 两点之间当前帧数大于总帧数的时候,则说明已经完成移动
+        if (currentCount >= count) {
+            clearInterval(intervalFlag);
+        } else {
+            // 动画移动
+            currentCount++;
+            let x = linear(_centerPoint.x, _newPoint.x, currentCount, count);
+            let y = linear(_centerPoint.y, _newPoint.y, currentCount, count);
+            // 根据平面坐标转化为球面坐标
+            let pos = map.getMapType().getProjection().pointToLngLat(new BMap.Pixel(x, y));
+            map.centerAndZoom(pos, map.getZoom());
+        }
+    }, timer);
+}
+
+// 线性计算
+function linear(initPos, targetPos, currentCount, count) {
+    return (targetPos - initPos) * currentCount / count + initPos;
+}
+
+// 设置方向
+function setRotation(curPos, targetPos, marker) {
+    let deg = 0;
+    curPos = map.pointToPixel(curPos);
+    targetPos = map.pointToPixel(targetPos);
+    if (targetPos.x != curPos.x) {
+        let tan = (targetPos.y - curPos.y) / (targetPos.x - curPos.x), atan = Math.atan(tan);
+        deg = atan * 360 / (2 * Math.PI);
+        if (targetPos.x < curPos.x) {
+            deg = -deg + 90 + 90;
+        } else {
+            deg = -deg;
+        }
+        marker.setRotation(-deg);
+    } else {
+        let disy = targetPos.y - curPos.y;
+        let bias = 0;
+        if (disy > 0) {
+            bias = -1;
+        } else {
+            bias = 1;
+        }
+        marker.setRotation(-bias * 90);
+    }
+}

+ 86 - 11
src/main/resources/templates/map.html

@@ -1,5 +1,6 @@
 <!DOCTYPE html>
-<html lang="zh-CN">
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org">
 <head>
     <meta charset="UTF-8"/>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
@@ -8,6 +9,7 @@
 
     <script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=Z2045g3IUDzxUdIWhvjRi2IqMQTYXWrX"></script>
     <script type="text/javascript" src="http://api.map.baidu.com/library/CurveLine/1.5/src/CurveLine.min.js"></script>
+    <script type="text/javascript" src="../static/js/map.js" th:src="@{/js/map.js}"></script>
     <style type="text/css">
         body, html {
             width: 100%;
@@ -29,21 +31,33 @@
         .BMapLabel {
             border-radius: 20px 20px 20px 20px;
         }
+
+        .anchorBL {
+            display: none;
+        }
+
+        .BMap_cpyCtrl {
+            display: none;
+        }
     </style>
 </head>
 <body>
 <div id="map"></div>
 
 <script type="text/javascript">
-    var top_left_control = new BMap.ScaleControl({anchor: BMAP_ANCHOR_TOP_LEFT});// 左上角,添加比例尺
-    var top_left_navigation = new BMap.NavigationControl();  //左上角,添加默认缩放平移控件
-    var top_right_navigation = new BMap.NavigationControl({anchor: BMAP_ANCHOR_TOP_RIGHT, type: BMAP_NAVIGATION_CONTROL_SMALL}); //右上角,仅包含平移和缩放按钮
+    let top_left_control = new BMap.ScaleControl({anchor: BMAP_ANCHOR_TOP_LEFT});  // 左上角,添加比例尺
+    let top_left_navigation = new BMap.NavigationControl();  // 左上角,添加默认缩放平移控件
+    let top_right_navigation = new BMap.NavigationControl({anchor: BMAP_ANCHOR_TOP_RIGHT, type: BMAP_NAVIGATION_CONTROL_SMALL}); //右上角,仅包含平移和缩放按钮
     /*缩放控件type有四种类型:
     BMAP_NAVIGATION_CONTROL_SMALL:仅包含平移和缩放按钮;BMAP_NAVIGATION_CONTROL_PAN:仅包含平移按钮;BMAP_NAVIGATION_CONTROL_ZOOM:仅包含缩放按钮*/
+
     // 百度地图 API 功能
-    var map = new BMap.Map("map");
+    let map = new BMap.Map("map");
     // 初始化地图,设置中心点坐标和地图级别
-    var point = new BMap.Point(116.404, 39.915);
+    let point = new BMap.Point(116.331398, 39.897445);
+    // IP 定位
+    let myCity = new BMap.LocalCity();
+    myCity.get(getCity);
     map.centerAndZoom(point, 18);
     // 添加地图类型控件
     map.addControl(new BMap.MapTypeControl({mapTypes: [BMAP_NORMAL_MAP, BMAP_HYBRID_MAP]}));
@@ -51,11 +65,10 @@
     map.enableScrollWheelZoom(true);     // 开启鼠标滚轮缩放
     map.addControl(top_left_control);
     map.addControl(top_left_navigation);
-    map.addControl(top_right_navigation);
 
-    var icon = new BMap.Icon("https://www.easyicon.net/api/resizeApi.php?id=1223994&size=32", new BMap.Size(64, 64));
-    var marker = new BMap.Marker(point, {icon: icon});  // 创建标注
-    var label = new BMap.Label("物联网汽车:" + new Date(), {offset: new BMap.Size(40, 10)});
+    let icon = new BMap.Icon("/images/car.png", new BMap.Size(64, 64));
+    let marker = new BMap.Marker(point, {icon: icon});  // 创建标注
+    let label = new BMap.Label("物联网汽车:" + new Date(), {offset: new BMap.Size(52, 0)});
     label.setStyle({
         color: "#2b2b2b",
         fontSize: "14px",
@@ -67,8 +80,70 @@
 
     marker.setLabel(label);                 // 创建标注的文字标签
     map.addOverlay(marker);                 // 将标注添加到地图中
-    var circle = new BMap.Circle(point, 160, {fillColor: "gray", strokeWeight: 1, fillOpacity: 0.3, strokeOpacity: 0.3});
+    let circle = new BMap.Circle(point, 100, {fillColor: "gray", strokeWeight: 1, fillOpacity: 0.3, strokeOpacity: 0.3});
     map.addOverlay(circle);
+
+    function updateLocation(data) {
+        let device = JSON.parse(data);
+        let newPoint = new BMap.Point(device.latitude, device.longitude);
+        label.setContent("物联网汽车:" + device.remark);
+        // 坐标转换
+        let convertor = new BMap.Convertor();
+        let pointArr = [];
+        pointArr.push(newPoint);
+        convertor.translate(pointArr, 1, 5, translateCallback);
+    }
+
+    // 坐标转换完之后的回调函数
+    translateCallback = function (data) {
+        if (data.status === 0) {
+            moveCar(point, data.points[0], 50, marker, 10);
+            point = data.points[0];
+        }
+    };
+
+    let websocket = null;
+    if ('WebSocket' in window) {
+        websocket = new WebSocket("ws://127.0.0.1/ws/map");
+    } else {
+        alert('抱歉,浏览器不支持,请更换浏览器!')
+    }
+
+    websocket.onerror = function () {
+        setMessageInnerHTML("连接错误");
+    };
+
+    websocket.onopen = function (event) {
+        setMessageInnerHTML("连接打开");
+    };
+
+    websocket.onmessage = function (event) {
+        console.log(event.data);
+        //showMessage(JSON.parse(event.data));
+        updateLocation(event.data);
+    };
+
+    websocket.onclose = function () {
+        setMessageInnerHTML("连接关闭");
+        if (window.confirm('连接已关闭,是否刷新页面?')) {
+            window.location.reload();
+            return true;
+        } else {
+            return false;
+        }
+    };
+
+    window.onbeforeunload = function () {
+        closeWebSocket();
+    };
+
+    function setMessageInnerHTML(message) {
+        console.log(message);
+    }
+
+    function closeWebSocket() {
+        websocket.close();
+    }
 </script>
 </body>
 </html>