地理围栏(Geo-fencing)是LBS 的一种新应用,就是用一个虚拟的栅栏围出一个虚拟地理边界。当手机进入、离开某个特定地理区域,或在该区域内活动时,后台可以感知到这一变化,同时手机可以接收自动通知和警告。地理围栏技术融入生活,可以使得生活效率更加高效同时可以使得生活更加安全。有了地理围栏技术,位置社交网站就可以帮助用户在进入某一地区时自动登记。现阶段的地理围栏应用十分有限,相关资料与资源也不充足,本次实习致力于打造一个能够供个人使用的简单的地理围栏应用,并且能够在此基础上升级为企业、人员管理等多人使用的移动应用。
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.3 地理围栏的设计与管理
地理围栏的设计主要是通过百度地图api的多边形进行实现。多边形由大于等于三个点构成,可以增加、删除、编辑边界点,除此之外,可以直接在地图上拖动边界点进行编辑,点击边界虚化中点可以在边界中点添加一个边界点。这样可以实现十分简单便捷地设计各种形状的地理围栏。
2.4 判断点是否在多边形内的算法设计
判断点在多边形内部的方法有很多,不过有的只适用于凸多边形,但是对于地理围栏应用,围栏不一定宗室凸多边形,所以算法需要能够适用于所有多边形,本次实习使用的方法是引射线法。引射线法:从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果有奇数个交点,则说明在内部,如果有偶数个交点,则说明在外部。
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直接在页面中的<script>标签中注册即可,由于是vue,需要在main.js中全局引入,相关代码为:import BaiduMap from 'vue-baidu-map'; Vue.use(BaiduMap, { ak: '申请的百度地图API的AK' });
然后是对uview进行注册配置,项目初始是没有uview-ui这个文件夹的,需要下载后复制过来,这一点也体现了uView使用的便利之处,只需要将文件夹复制到项目中,配置一下就能使用了。配置主要是APP.vue中引入全局css配置,在main.js中引入组件库,在uni.scss中配置内置样式。相关代码分别为:
APP.vue:<style lang = "scss"> @import "uview-ui/index.scss"; </style>
main.js:import uView from 'uview-ui'; Vue.use(uView);
uni.scss:@import 'uview-ui/theme.scss';
百度地图api的初始化与使用
经过上述步骤后,就能够使用uView组件库和百度地图api了。首先根据原型图,应用最上方是显示地图,这里需要使用百度地图,和html不一样,html是直接使用var map = new BMap.Map("container");来进行地图创建,但是vue不一样,需要将所有的创建地图方法封装到地图初始化函数中,这也就导致了vue相比较html而言,对地图的操作没有那么方便,因为html新建的地图map相当于一个全局变量,在整个页面中都可以直接对map进行修改,而vue只在地图初始化的函数中能够对地图进行操作,对地图的修改只能通过地图组件函数将map传入函数才能够修改。初始化函数和html的写法中几乎是一样的:创建地图--设置中心点和缩放--添加覆盖物--添加控件--为覆盖物和控件添加函数。
首先创建地图并设置中心点,部分代码如下。
var point = new BMap.Point(114.623556,30.463753) // 新建一个点作为地图中心点
map.centerAndZoom(point, 16) // 设置地图中心点和缩放级别
map.enableScrollWheelZoom(); // 开启地图缩放
var marker = new BMap.Marker(point, { enableDragging:true}) // 创建地图中心点的可拖标注
map.addOverlay(marker) // 将标注添加到地图中
然后是添加定位控件,定位控件主要是手动定位使用,点击后,如果定位成功,则地图中心跳转到定位的位置,同时为了便利用户,可以弹出一个模态框告知用户位置信息,并提供复制位置信息的选项。因此需要在页面添加一个模态框,内容为定位地址,定位成功时显示,然后定位成功函数中修改位置并打开模态框,最后还需要一个复制函数。
模态框设置如下,其中参数分别为:显示的开关,标题,内容,是否有取消按钮,取消按钮的名称,是否交换确认取消按钮的顺序,确认按钮的调用函数,取消按钮的调用函数。
<u-modal :show="show_location" title="定位信息" :content='current_location' :showCancelButton="true" cancelText="复制" :buttonReverse="true" @confirm="close_loc_info()" @cancel="copy()"></u-modal>
确认按钮的调用函数比较简单,只需要将显示开关关闭即可,取消按钮(复制按钮)的函数为复制模态框中的位置变量,部分代码如下。
let input = document.createElement('input')
input.value = this.current_location
input.id = 'creatDom'
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
紧接着是定位控件的添加与定位成功函数的编写,将定位信息保存,然后打开模态框即可。这里需要注意的是,因为调用了addEventListener函数,在这个函数中创建新的函数,需要将this指针保存起来,否则在嵌套函数中使用this是找不到页面变量的。
let _this = this // 保存this,否则嵌套函数中的this指向不到data
var geolocationControl = new BMap.GeolocationControl(); // 创建定位控件
geolocationControl.addEventListener("locationSuccess", function(e){ // 定位成功事件
……
address += e.addressComponent.province;
address += "\n经度:"+ e.point.lng + "\n纬度:" + e.point.lat
_this.current_location = address
_this.show_location = true
});
map.addControl(geolocationControl);
最后是在地图中添加多边形,也就是地理围栏的关键点,任意形状的多边形。如果是手动去写这个多边形的逻辑是很麻烦的,因为要创建点,设置点的参数,设置边界中心点,设置多边形等等,但是百度地图在vue中封装了多边形,使用起来很简单,只需要设置多边形的边界点的列表即可做到对多边形的编辑与修改。
使用组件为bm-polygon,一般使用只需要关注path即可,这就是多边形的边界点列表。
<bm-polygon :path="form.polygonPath" stroke-color="blue" :stroke-opacity="0.5" :stroke-weight="2" :editing="true" @lineupdate="updatePolygonPath"/>
同时对地图还可以添加一些实用控件,例如混合地图,比例尺等等,调用百度地图API的接口即可。同时为了方便用户,还可以设置点击地图操作,点击后弹出点击处的经纬度,这样可以让用户更容易要设置的定位围栏边界。这里$refs.uToast为uView组件,效果是弹出一个很快消失的提示。
map.addEventListener("click", function(e){
var content = "经度:" + e.point.lng+ "\n纬度:" +e.point.lat;
_this.$refs.uToast.show({type: 'success',message: content})
})‘
最终地图的显示效果如下。地图中心的标注可以拖动,边界点可以拖动并实时修改,点击地图会弹出点击处的经纬度信息。
(3)APP页面搭建
这部分主要就是使用第三方组件库搭建页面,主要是一些html和css语言,没有太多的技术含量,最终页面搭建如下。其中区域管理部分是一个list,这个list关联的就是区域的边界点,内置组件分别为两个输入框和一个删除按钮,能够实时修改区域,添加点按钮会在list中添加一个点,这个点为当前定位坐标。下方就是相当于一个控制台,能够实时显示定位数据与位置相对区域的状态。(下图因为还没有调用定位接口,位置判断是写死的,实际应该为:"当前位置在区域内")
系统定位与位置判定
最后就是对当前位置的判定了,这里需要实时定位,之前在地图做的定位是手动定位,而应用功能是需要实时监控当前状态,因此页面需要不断执行一个函数,这个函数的功能就是1.定位,获取当前位置2.判断是否在区域内,这部分放到后端执行,因此需要做的就是将当前的状态信息发送到后端,并对返回数据进行处理。对于要发送到后端的数据,由于需要判定是进入还是离开区域,所以需要发送一个状态代表上次判断的结果,如果上次判定在区域外,此次判定在区域内则可以认为用户进入了区域,其他判断同理。这里定义返回code,100 代表之前在区域外,但现在进入区域了,状态修改为0,弹出提示。 200 代表状态为在区域内。状态修改或保持为0。300 代表原来在区域内,现在在区域外,状态修改为1,弹出提示。400 代表状态为在区域外,状态保持或修改为1。这里接口访问的是域名,如果是本地接口需要将域名改成localhost。
let _this = this
uni.getLocation({ type: 'wgs84',
success: function (res) {
_this.form.myposition_lng = res.longitude
_this.form.myposition_lat = res.latitude
uni.request({ url: 'http://----.ink:9875/judge/location', data: _this.form, dataType: 'json', header: { 'content-type': 'application/json'}, method:'POST', success: (res) => {
if(res.data.code == "100"){
// 修改信息等等操作
……
然后是在页面创建的时候就对这个函数进行循环,不断定位+访问端口确认位置。方式是在页面创建的时候设定定时器循环执行上一步写的函数,这里设定定时器时间为5s,即每5s定位并判断当前位置是否在区域内,也可以根据服务器的承载能力适当把频率提高或者降低。
mounted() {
this.refreshLocation()
this.refreshHeader = setInterval(() => {
this.refreshLocation()
}, 5000)
}
(4)后端开发
Springboot开发相较于传统Spring开发来说十分便利,首先是写上接口名,用@RequestMapping("/xxx")注解来注明访问类的地址,对于类的每一个函数,如果是post方法,则使用@PostMapping("/xxx")来注明函数的访问地址。根据之前在前端项目中的定义,接收的数据有区域边界点的列表,上一个状态,当前坐标。根据判断结果返回code分别为100、200、300、400,分别对应进入区域、保持在区域内、出区域、保持在区域内。
由于同源策略,进行网络请求的时候,如果协议、域名、端口号任意一个不一样则会产生跨域问题,导致网络请求失败。同源策略,是由 Netscape 提出的一个安全策略,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,则浏览器的正常功能可能都会受到影响,现在所有支持JavaScript的浏览器都会使用这个策略。所以在开发环境下需要解决跨域问题,在前端可用代理来解决,后端可以用跨域注解@CrossOrigin来解决。但是对于代理方式,真正去部署到服务器的时候,这种方式就会失效,因为部署的时候代理文件都不会放到服务器上,需要使用Nginx来解决,而使用跨域注解的方式对于个人开发而言,不仅简单,而且部署到服务器后依旧能跨域,所以本项目跨域在后端使用跨域注解。不过如果产品上市的话就不能在后端使用跨域注解了,那样可能会导致恶意不断调用接口导致服务器崩溃的情况。目前接口调用的是判断位置的函数judgeCurrentLocation.judgeStatue(form),根据不同结果返回不同的code,Result类是封装的返回变量,包含code、message和data。
@RestController
@CrossOrigin
@RequestMapping("/judge")
public class judgeController {
@PostMapping("/location")
public Result<?> judgeLocation(@RequestBody Form form){
int status = judgeCurrentLocation.judgeStatue(form);
if(status == 0){ return Result.success("100");}
……
最后就是判断位置是否在区域内的函数,也就是检测点是否在任意多边形内部的方法。使用的是射线法,主要思路就是经过这个点做一条射线,计算这条射线和多边形交点的个数,如果交点个数为偶数则在多边形外部,如果是奇数则在三角形内部,如果点落在多边形上认为点在三角形内部。
部分代码:
for (int i = 1; i <= polygonSidesCount; ++i) {
.…..
else {
// 如果线段终点的x坐标对应的平行线上的点低于终点的y坐标
if (point.x == p2.x && point.y <= p2.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)) {
intersectCount++; // x坐标位于线段p1p3关于x轴的投影中,穿过端点一次
}
else {
intersectCount += 2; // 射线穿过两次
}
}
}
p1 = p2;
}
return intersectCount % 2 != 0; // 奇数在多边形内,偶数在多边形外
(5)服务器部署与APP打包
由于是移动APP,如果不将后端项目打包到服务器,那么这个移动应用是没办法使用的。Springboot发布到服务器还是挺简单的。首先使用Maven打包,点击clean然后package。
打包后能在项目路径下得到一个target文件夹,其中会有一个jar包,只需要将这个jar包和application.properties(端口配置)上传到服务器,然后在服务器的终端运行命令nohup java -jar demo-0.0.1-SNAPSHOT.jar & 即可在服务器部署后端项目。这里demo-0.0.1-SNAPSHOT是默认打包后的名字,名字可以在pom.xml的<filename></filename>标签中修改。
后端发布到服务器后前端项目接口路由的localhost就可以换成----.ink了,同时本地后端项目也不再需要启动。
对于uni-app项目的打包则更加简单,在APP云打包中配置好即可一键打包得到apk安装包。
APP运行效果
地理围栏效果
点击地图显示点击处经纬度
出入区域提示
手动定位效果
卫星地图
使用帮助
项目代码
map.vue
<template>
<view>
<u-toast ref="uToast"></u-toast>
<u-modal
:show="show_location"
title="定位信息"
:content='current_location'
:showCancelButton="true"
cancelText="复制"
:buttonReverse="true"
@confirm="close_loc_info()"
@cancel="copy()">
</u-modal>
<u-modal
:show="show_tip"
title="提示"
:content='position_tip'
@confirm="close_tip_info()">
</u-modal>
<baidu-map class="map" @ready="handler">
<bm-scale anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-scale>
<bm-navigation anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-navigation>
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']" anchor="BMAP_ANCHOR_TOP_LEFT"></bm-map-type>
<bm-polygon :path="form.polygonPath" stroke-color="blue" :stroke-opacity="0.5" :stroke-weight="2" :editing="true" @lineupdate="updatePolygonPath"/>
<bm-panorama anchor="BMAP_ANCHOR_BOTTOM_RIGHT"></bm-panorama>
</baidu-map>
<view style="margin: 2%;"></view>
<view style="text-align: center;">
<table style='text-align: center; width: 90%;border: 3px solid #ebeef5;border-radius: 10rpx;'>
<tr>
<td><view style="font-size: large;font-weight: bold; color: #666666;">区域管理</view></td>
</tr>
<tr><u-line ></u-line></tr>
<tr>
<td>
<table style='width: 100%;'>
<tr>
<td><view>经度</view></td>
<td><view style="margin-left: 30%;">纬度</view></td>
<td><view style="margin-left: 35%;">操作</view></td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<u-list height="300rpx">
<u-list-item :showScrollbar="true" v-for="(item, index) in form.polygonPath" :key="index">
<table style="text-align: center;margin: 1%;border: 1px solid #ebeef5;border-radius: 10rpx;">
<tr>
<td>
<u--input placeholder="请输入经度" border="surround" v-model="form.polygonPath[index].lng" style='width: 80%;'></u--input>
</td>
<td>
<u--input placeholder="请输入纬度" border="surround" v-model="form.polygonPath[index].lat" style='width: 80%;'></u--input>
</td>
<td><u-button type="error" size="small" text="删除" @click="delete_point(index)"></u-button></td>
</tr>
</table>
</u-list-item>
</u-list>
</td>
</tr>
</table>
</view>
<view style="margin: 1%;"></view>
<u-button style="width: 90%;" type="primary" :plain="true" text="添加点" @click='addPoint()'></u-button>
<view style="margin: 2%;"></view>
<table style="width: 91%;">
<tr>
<td>
<u-textarea
height="60"
v-model="refreshed_position"
placeholder="这里将显示你的位置信息"
disabled
>
</u-textarea>
</td>
</tr>
<tr><td><view style="font-size: xx-small;margin-top: 1%;color: #0077ff;" @click="help()">帮助</view></td></tr>
</table>
</view>
</template>
<script>
export default {
data() {
return {
form: {
status: '-1',
polygonPath: [
{lng: 114.616531, lat: 30.465807},
{lng: 114.628425, lat: 30.466399},
{lng: 114.629431, lat: 30.460578},
{lng: 114.622837, lat: 30.460640},
{lng: 114.616729, lat: 30.463473},
],
myposition_lng: '',
myposition_lat: '',
},
current_location: "请先定位",
show_location: false,
refreshHeader: '',
refreshed_position: '这里将显示你的位置信息',
show_tip: false,
position_tip: '状态读取中'
}
},
methods: {
handler({BMap, map}){
let _this = this // 保存this,否则嵌套函数中的this指向不到data
var point = new BMap.Point(114.623556,30.463753) // 新建一个点作为地图中心点
map.centerAndZoom(point, 16) // 设置地图中心点和缩放级别
map.enableScrollWheelZoom(); // 开启地图缩放
var marker = new BMap.Marker(point, {
enableDragging:true
}) // 创建地图中心点的标注
map.addOverlay(marker) // 将标注添加到地图中
// 创建定位控件
var geolocationControl = new BMap.GeolocationControl();
// 定位成功事件
geolocationControl.addEventListener("locationSuccess", function(e){
var address = '';
address += e.addressComponent.province;
address += e.addressComponent.city;
address += e.addressComponent.district;
address += "\n经度:"+ e.point.lng + "\n纬度:" + e.point.lat
_this.current_location = address
_this.show_location = true
});
// 定位失败事件
geolocationControl.addEventListener("locationError",function(e){
_this.$refs.uToast.show({
type: 'error',
message: "定位失败,请检查网络或权限",
})
});
map.addControl(geolocationControl);
// 地图点击事件
map.addEventListener("click", function(e){
var content = "经度:" + e.point.lng + "\n纬度:" + e.point.lat;
_this.$refs.uToast.show({
type: 'success',
message: content,
})
})
},
updatePolygonPath (e) {
this.form.polygonPath = e.target.getPath()
},
addPoint(){
this.form.polygonPath.push({lng: this.form.myposition_lng, lat: this.form.myposition_lat})
},
copy () {
// 复制
let input = document.createElement('input')
input.value = this.current_location
input.id = 'creatDom'
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
this.show_location = false
this.$refs.uToast.show({
type: 'success',
message: "复制成功",
})
},
close_loc_info() {
this.show_location = false
},
close_tip_info(){
this.show_tip = false
},
delete_point(index){
this.form.polygonPath.splice(index, 1)
},
refreshLocation(){
let _this = this
uni.getLocation({
type: 'wgs84',
success: function (res) {
// console.log('位置已刷新');
// _this.refreshed_position = "当前位置为:\n经度:" + res.longitude + " 纬度:" + res.latitude;
// _this.refreshed_position += _this.position_status
_this.form.myposition_lng = res.longitude
_this.form.myposition_lat = res.latitude
uni.request({
// url: 'http://localhost:9875/judge/location', // 本地服务器端口地址
url: 'http://daisuki.ink:9875/judge/location',
data: _this.form,
dataType: 'json',
header: {
'content-type': 'application/json'
},
method:'POST',
success: (res) => {
// 这里返回的code
// 100 代表之前在区域外,但现在进入区域了。状态修改为0,弹出提示
// 200 代表状态为在区域内。状态修改或保持为0
// 300 代表原来在区域内,现在在区域外。状态修改为1,弹出提示
// 400 代表状态为在区域外。状态保持或修改为1
if(res.data.code == "100"){
_this.status = '0'
// 这里refreshed_position需要刷新,避免闪屏和多加
_this.refreshed_position = "当前位置为:\n经度:" + _this.form.myposition_lng + " 纬度:" + _this.form.myposition_lat;
_this.refreshed_position += "\n当前位置在区域内"
_this.position_tip = "您已进入区域"
_this.show_tip = true
}
else if(res.data.code == "200"){
_this.status = '0'
_this.refreshed_position = "当前位置为:\n经度:" + _this.form.myposition_lng + " 纬度:" + _this.form.myposition_lat;
_this.refreshed_position += "\n当前位置在区域内"
}
else if(res.data.code == "300"){
_this.status = '1'
_this.refreshed_position = "当前位置为:\n经度:" + _this.form.myposition_lng + " 纬度:" + _this.form.myposition_lat;
_this.refreshed_position += "\n当前位置在区域外"
_this.position_tip = "您已离开区域"
_this.show_tip = true
}
else if(res.data.code == "400"){
_this.status = '1'
_this.refreshed_position = "当前位置为:\n经度:" + _this.form.myposition_lng + " 纬度:" + _this.form.myposition_lat;
_this.refreshed_position += "\n当前位置在区域外"
}
else{
// 出现错误(网络等)
_this.$refs.uToast.show({
type: 'error',
message: "判断位置信息失败",
})
}
}
});
}
})
},
help(){
// console.log("help")
uni.navigateTo({
url: '/pages/helpInfo/helpInfo',
});
}
},
mounted() {
this.refreshLocation()
this.refreshHeader = setInterval(() => {
this.refreshLocation()
}, 5000)
}
}
</script>
<style>
table{margin:0 auto;}
.map {
width: 100%;
height: 400px;
}
</style>
helpInfo.vue
<template>
<view>
<view style="margin: 5%;text-align: center;font-size: large;color: #666666;font-weight: bold">欢迎使用地理围栏</view>
<u-collapse accordion>
<u-collapse-item title="如何使用">
<text class="u-collapse-content">
在区域管理或者地图界面添加并编辑围栏形状,当您进入或者离开您设置的区域时,您会接收到消息通知,同时在最下方的控制台能够看到您当前的状态
</text>
</u-collapse-item>
<u-collapse-item title="如何管理围栏">
<text class="u-collapse-content">
在区域管理处您可以管理区域的边界点,包括修改位置,删除位置和添加位置,添加点的默认位置为您当前的定位,初始围栏默认为中国地质大学(武汉)未来城校区。同时在地图上也可以拖动边界点,点击边界线中间的虚拟点也会添加一个边界点
</text>
</u-collapse-item>
<u-collapse-item title="如何定位">
<text class="u-collapse-content">
定位有两种方式,在地图左下角有定位图标,此时的定位为百度地图的坐标系,点击后跳转到您的位置并且可以对信息进行复制。同时系统后台也在实时定位(手机系统定位),此时的定位信息是wgs84坐标系
</text>
</u-collapse-item>
<u-collapse-item title="如何获取地图上某个点的位置信息">
<text class="u-collapse-content">
只需要点击地图即可弹出点击处的经纬度
</text>
</u-collapse-item>
<u-collapse-item title="常见问题">
<text class="u-collapse-content">
地图围栏边界点无法点击或拖动
因为平台不同可能会出现这个问题,如果地图无法编辑区域,可以在区域管理中编辑区域,能够做到实时刷新。
</text>
</u-collapse-item>
</u-collapse>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
back(){
uni.navigateBack({
delta: 1
});
}
}
}
</script>
<style>
</style>
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<Point> 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<Point2D.Double> 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<Point> polygonPath, Point position){
Point2D.Double point = new Point2D.Double(Double.parseDouble(position.lng), Double.parseDouble(position.lat));
List<Point2D.Double> 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;
}
}
}
}