一、引言
地理围栏(Geo-fencing)是LBS 的一种新应用,就是用一个虚拟的栅栏围出一个虚拟地理边界。当手机进入、离开某个特定地理区域,或在该区域内活动时,后台可以感知到这一变化,同时手机可以接收自动通知和警告。地理围栏技术融入生活,可以使得生活效率更加高效同时可以使得生活更加安全。有了地理围栏技术,位置社交网站就可以帮助用户在进入某一地区时自动登记。现阶段的地理围栏应用十分有限,相关资料与资源也不充足,本次实习致力于打造一个能够供个人使用的简单的地理围栏应用,并且能够在此基础上升级为企业、人员管理等多人使用的移动应用。
二、关键技术方案
2.1 uni-app开发
uni-app 是一个使用 Vue.js (opens new window)开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。本次实习使用Hbuilder X(一个轻如编辑器、强如IDE的合体版本)进行uni-app的开发。
2.2 GPS定位技术
通过Android/iOS/windows phone 提供的API 直接获取位置。也可以调用百度地图api的接口直接获取当前的地理位置。不过需要注意的是由于各大坐标系不一样,百度地理api定位的坐标系和手机系统定位有些许差别。本次实习中手机系统定位使用的坐标系是wgs84坐标系。
2.4 地理围栏的设计与管理
地理围栏的设计主要是通过百度地图api的多边形进行实现。多边形由大于等于三个点构成,可以增加、删除、编辑边界点,除此之外,可以直接在地图上拖动边界点进行编辑,点击边界虚化中点可以在边界中点添加一个边界点。这样可以实现十分简单便捷地设计各种形状的地理围栏。
2.5 判断点是否在多边形内的算法设计
判断点在多边形内部的方法有很多,不过有的只适用于凸多边形,但是对于地理围栏应用,围栏不一定宗室凸多边形,所以算法需要能够适用于所有多边形,本次实习使用的方法是引射线法。引射线法:从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果有奇数个交点,则说明在内部,如果有偶数个交点,则说明在外部。
三、系统分析与设计
3.1 需求定义
1、功能需求
·地理围栏的构建
功能描述:用户能够构建并管理任意形状的地理围栏。
基本流程:在区域管理处用户可以管理区域的边界点,包括修改位置,删除位置和添加位置,添加点的默认位置为用户当前的定位。同时在地图上也可以拖动边界点,点击边界线中间的虚拟点也会添加一个边界点,实现更为便利的围栏管理
·定位
功能描述:用户授权后能够实时对用户定位。
基本流程:系统后台会通过系统GPS实时定位并显示,地图上也会有定位按钮,用户点击后地图会跳转到用户当前位置,同时弹出详细信息窗口供用户复制。
·进入/离开地理围栏通知
功能描述:提示用户进出地理围栏。
基本流程:当用户进入或者离开地理围栏的时候,能够弹出通知告知用户。
·时刻显示用户当前状态
功能描述:用户能够实时看到自己当前的状态。
基本流程:用户能够直接查看自己是在地理围栏内部还是外部。
·其他功能
功能描述:比例尺、缩放、卫星地图、全景地图等。
基本流程:调用百度地图api的多种接口能够使地图更加实用。
四、实现与实验
4.1 软件实现
uni-app开发
为了实现多端兼容,综合考虑编译速度、运行性能等因素,uni-app 约定了如下开发规范:
(1)页面文件遵循 Vue 单文件组件 (SFC) 规范(opens new window)
(2)组件标签靠近小程序规范,详见uni-app 组件规范
(3)接口能力(JS API)靠近微信小程序规范,但需将前缀 wx 替换为 uni
(4)数据绑定及事件处理同 Vue.js 规范,同时补充了App及页面的生命周期
(5)为兼容多端运行,建议使用flex布局进行开发
开发过程类似于vue的开发流程。在main.js中配置相关-创建vue页面并边界-在pages中注册页面。开发使用Hbuilder X界面如下:
开发结束并测试无误后可以进行APP打包。在发行-APP云打包中配置相关信息打包即可生成apk文件。
Springboot开发
Spring框架是Java平台上的一种开源应用框架,提供具有控制反转特性的容器。Spring框架为开发提供了一系列的解决方案,比如利用控制反转的核心特性,并通过依赖注入实现控制反转来实现管理对象生命周期容器化,利用面向切面编程进行声明式的事务管理,整合多种持久化技术管理数据访问,提供大量优秀的Web框架方便开发等等。
SpringBoot所具备的特征有:
(1)可以创建独立的Spring应用程序,并且基于其Maven或Gradle插件,可以创建可执行的JARs和WARs;
(2)内嵌Tomcat或Jetty等Servlet容器;
(3)提供自动配置的“starter”项目对象模型(POMS)以简化Maven配置;
(4)尽可能自动配置Spring容器;
(5)提供准备好的特性,如指标、健康检查和外部化配置;
(6)绝对没有代码生成,不需要XML配置。
开发springboot需要的编写的目录如下。其中common中为一些公共资源,controller中为后端接口,entity中为一些实体类,function中为一些函数方法。DemoApplication为springboot的启动类。
4.2 实验步骤
(1)设计界面原型
首先用Axure绘制APP的原型图。本APP主打简单易操作,因为用户需要操作的地方主要就是编辑区域,其他检测定位等操作都是系统后台进行,所以界面并不复杂。APP主页面最上方就是地图,这里能够显示用户创建的区域,并且能够对多边形进行编辑,同时还能够手动定位。在中部是区域管理,更详细的区域修改在这个地方,能够更详细地编辑边界点的信息。在下方是当前的状态信息,告知用户当前位置以及当前位置相较于区域的状态。然后就是帮助按钮,点击后跳转到帮助页面。当用户进入/离开设置的区域的时候,能够弹出提示。
(2)Uni-app开发
Uni-app项目构成
Uni-app项目构成如下
pages文件夹中为编辑的页面,uview-ui是使用的第三方组件库,main.js是所有页面公共的配置,所有页面需要使用的元素需要在这个文件中全局配置,App.vue中可以配置全局css,在pages.json中配置并注册所有页面,只有注册的页面才能够被访问,其中页面数组第一项为应用启动页面。Uni.scss为uni-app内置的常用样式变量。
第三方库的注册使用
首先要使用第三方库就要对相应的库进行注册引入。对于百度地图api,首先使用命令$ npm install vue-baidu-map –save 对百度地图api进行引入。这里和html开发不一样,html直接在页面中的
helpInfo.vue
欢迎使用地理围栏 在区域管理或者地图界面添加并编辑围栏形状,当您进入或者离开您设置的区域时,您会接收到消息通知,同时在最下方的控制台能够看到您当前的状态 在区域管理处您可以管理区域的边界点,包括修改位置,删除位置和添加位置,添加点的默认位置为您当前的定位,初始围栏默认为中国地质大学(武汉)未来城校区。同时在地图上也可以拖动边界点,点击边界线中间的虚拟点也会添加一个边界点 定位有两种方式,在地图左下角有定位图标,此时的定位为百度地图的坐标系,点击后跳转到您的位置并且可以对信息进行复制。同时系统后台也在实时定位(手机系统定位),此时的定位信息是wgs84坐标系 只需要点击地图即可弹出点击处的经纬度 地图围栏边界点无法点击或拖动 因为平台不同可能会出现这个问题,如果地图无法编辑区域,可以在区域管理中编辑区域,能够做到实时刷新。
judgeController.java
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.entity.Form;
import org.springframework.web.bind.annotation.*;
import com.example.demo.function.judgeCurrentLocation;
@RestController
@CrossOrigin
@RequestMapping("/judge")
public class judgeController {
@PostMapping("/location")
public Result> judgeLocation(@RequestBody Form form){
int status = judgeCurrentLocation.judgeStatue(form);
if(status == 0){
// 之前在区域外,但现在进入区域了
System.out.println("用户已进入区域");
return Result.success("100");
}
else if(status == 1){
// 状态为在区域内
return Result.success("200");
}
else if(status == 2){
// 原来在区域内,现在在区域外
System.out.println("用户已离开区域");
return Result.success("300");
}
else{
// 状态为在区域外
return Result.success("400");
}
}
}
Form.java
package com.example.demo.entity;
import lombok.Data;
import java.util.List;
@Data
public class Form {
public String status; // 前面的状态 -1为初始状态,0在区域内,1在区域外
public List polygonPath;
public String myposition_lng;
public String myposition_lat;
}
Point.java
package com.example.demo.entity;
public class Point {
public String lng;
public String lat;
public Point(String lng, String lat) {
this.lng = lng;
this.lat = lat;
}
}
judgeCurrentLocation.java
package com.example.demo.function;
import com.example.demo.entity.Form;
import com.example.demo.entity.Point;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
public class judgeCurrentLocation {
public static boolean isPtInPoly(Point2D.Double point, List pts) {
int polygonSidesCount = pts.size(); // 如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
int intersectCount = 0; // 射线与多边形相交的次数
double precision = 2e-10; //浮点类型计算时候与0比较时候的容差
Point2D.Double p1, p2; // 相邻两点
p1 = pts.get(0); // 线段起点
for (int i = 1; i <= polygonSidesCount; ++i) {
// 按照点录入的顺序依次检查相邻两点组成的线段和当前的点的关系。
if (point.equals(p1)) {
// 如果当前点就是多边形的顶点之一,直接返回。
return true;
}
p2 = pts.get(i % polygonSidesCount); // 线段终点
if (point.x < Math.min(p1.x, p2.x) || point.x > Math.max(p1.x, p2.x)) {
// 点在x轴上的映射,明显超出了线段在x轴上的投影
p1 = p2;
continue;
}
if (point.x > Math.min(p1.x, p2.x) && point.x < Math.max(p1.x, p2.x)) {
// 当前点在线段于x轴上的投影内
if (point.y <= Math.max(p1.y, p2.y)) {
//当前点的 y 坐标小于 线段对y轴投影的最大值
if (p1.x == p2.x && point.y >= Math.min(p1.y, p2.y)) {
// 若线段同样是平行于y轴,则可以断定,当前点,在多边形的这条垂直于x轴的边上。
return true;
}
if (p1.y == p2.y) {
// 若线段为平行于x轴的水平线
if (p1.y == point.y) {
// 当前点正好位于该水平线上,直接返回该点位于多边形的一条边上。
return true;
} else {
// 如果当前点在水平线以下,增加一个交点。
++intersectCount;
}
} else {
// 如果不是水平线,则用两点式求当前点的x带入多边形线段的直线方程后,对应的y的坐标
double xInSideLineFormulaResultY = (point.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;
if (Math.abs(point.y - xInSideLineFormulaResultY) < precision) {
// 误差允许范围内,该点就在线段上,则表明,该点位于多边形的一个边上。
return true;
}
if (point.y < xInSideLineFormulaResultY) {
// 如果线段上取得的y比当前点的y要大,当前做向上的射线,肯定交于上方的一个点。
++intersectCount;
}
}
}
} else {
// 当前点不在线段投影到x轴的区间中
if (point.x == p2.x && point.y <= p2.y) {
// 但恰好位于线段终点的x坐标对应的平行于y轴上的线上的低于终点y的一点
// 此时检查下一点能否将其x边界包含。
Point2D.Double p3 = pts.get((i + 1) % polygonSidesCount);
if (point.x >= Math.min(p1.x, p3.x) && point.x <= Math.max(p1.x, p3.x)) {
// 若当前点的x坐标位于 p1和p3组成的线段关于x轴的投影中,则记为该点的射线只穿过端点一次。
++intersectCount;
} else {
// 若当前点的x坐标不能包含在p1和p3组成的线段关于x轴的投影中,则点射线通过的两条线段组成了一个弯折的部分,
// 此时我们记射线穿过该端点两次
intersectCount += 2;
}
// 此判断的核心思路是由点在两条线段的内部还是外部去思考得出的
}
}
// 进行下一个线段的判断
p1 = p2;
}
// 奇数在多边形内,偶数在多边形外
return intersectCount % 2 != 0;
}
public static boolean judgePosition(List polygonPath, Point position){
Point2D.Double point = new Point2D.Double(Double.parseDouble(position.lng), Double.parseDouble(position.lat));
List pts = new ArrayList<>();
for (Point value : polygonPath) {
pts.add(new Point2D.Double(Double.parseDouble(value.lng), Double.parseDouble(value.lat)));
}
return isPtInPoly(point,pts);
}
public static int judgeStatue(Form form){
Point point = new Point(form.myposition_lng,form.myposition_lng);
if(judgePosition(form.polygonPath,point)){
// 判断结果为在区域内
if(form.status.equals("1")){
// 之前在区域外,但现在进入区域了
return 0;
}
else{
// 状态为在区域内
return 1;
}
}
else{
// 判断结果为在区域外
if(form.status.equals("0")){
// 原来在区域内,现在在区域外
return 2;
}
else{
// 状态为在区域外
return 3;
}
}
}
}



