一、引言

地理围栏(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.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;
            }
        }
    }

}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐