
目录
射线法
原理
使用射线法来判断,目标点是否在多边形内部
这里简单说下,具体细节可以看这篇文章
原理很简单,从点引出一条射线,计算射线和多边形的交点数量。
交点数如果是 奇数 ,说明点 在 多边形内;如果是 偶数 ,则点 不在 多边形内。
射线方向没有要求,通常选择水平或垂直方向的射线,能够有效减少计算量。这里选择 向右的射线
然后就是遍历多边形的所有边,判断边线段和射线是否有交点,有交点就给相交数加 1。

还需要处理一些特殊情况,就是 射线刚好穿过多边形的顶点的情况
① 点的两边都在射线同一侧
② 点的两边在射线不同侧
③ 与多边形的一条边共线

此时可以使用 叉积 判断 目标点是否在边的左侧/右侧


简要逻辑代码
bash
const isPointInPolygon = (polygon, pt) => {
let count = 0;
for (let i = 0; i < polygon.length; i++) {
let a = polygon[i];
let b = polygon[(i + 1) % polygon.length];
if (a.y > b.y) {
[a, b] = [b, a];
}
if (a.y <= pt.y && b.y > pt.y) {
const crossProduct =
(pt.x - a.x) * (b.y - a.y) - (b.x - a.x) * (pt.y - a.y);
if (crossProduct === 0) {
return true;
}
if (crossProduct > 0) {
count++;
}
}
}
return count % 2 === 1;
};
小程序代码
项目结构树
bash
.
├── app.js
├── app.json
├── app.wxss
├── data
│ └── map_data.js
├── pages
│ ├── polygons
│ │ ├── polygons.js
│ │ ├── polygons.json
│ │ ├── polygons.wxml
│ │ └── polygons.wxss
│ └── range
│ ├── range.js
│ ├── range.json
│ ├── range.wxml
│ └── range.wxss
├── project.config.json
├── project.private.config.json
└── sitemap.json
调试基础库
3.8.0
小程序配置
app.json
bash
{
"pages": [
"pages/polygons/polygons",
"pages/range/range"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "点与多边形",
"navigationBarTextStyle": "black"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents",
"useExtendedLib": {
"weui": true
},
"resolveAlias": {
"@data/*": "data/*"
}
}
地图数据
data/map_data.js
bash
/* data/map_data.js */
// 地图相关
module.exports = {
// 地图部分参数
// 学校中心点坐标
longitude: 110.27672,
latitude: 25.0937,
// 是否展示 POI 点
enablepoi: true,
// 是否显示带有方向的当前定位点
showLocation: true,
// 缩放级别
scale: 16,
// 闭合多边形
points: [
{
"latitude": 25.098567,
"longitude": 110.280995
},
{
"latitude": 25.097711,
"longitude": 110.281167
},
{
"latitude": 25.096842,
"longitude": 110.281137
},
{
"latitude": 25.095527,
"longitude": 110.280778
},
{
"latitude": 25.092988,
"longitude": 110.279991
},
{
"latitude": 25.090947,
"longitude": 110.279089
},
{
"latitude": 25.088824,
"longitude": 110.277994
},
{
"latitude": 25.088921,
"longitude": 110.277342
},
{
"latitude": 25.088633,
"longitude": 110.277057
},
{
"latitude": 25.089162,
"longitude": 110.275070
},
{
"latitude": 25.090355,
"longitude": 110.272274
},
{
"latitude": 25.094758,
"longitude": 110.274553
},
{
"latitude": 25.095421,
"longitude": 110.275129
},
{
"latitude": 25.096114,
"longitude": 110.275212
},
{
"latitude": 25.097485,
"longitude": 110.277418
},
{
"latitude": 25.098553,
"longitude": 110.279565
},
{
"latitude": 25.098779,
"longitude": 110.280243
}
],
}
地图多边形
polygons.js
bash
// pages/polygons/polygons.js
import map_data from '@data/map_data'
Page({
/**
* 页面的初始数据
*/
data: {
Marker2_Activated: "https://3gimg.qq.com/lightmap/xcx/demoCenter/images/[email protected]",
Marker3_Activated: "https://3gimg.qq.com/lightmap/xcx/demoCenter/images/[email protected]",
// 地图中心点坐标
longitude: map_data.longitude,
latitude: map_data.latitude,
// 缩放级别
scale: map_data.scale,
// 标记点
markers: [],
// 多边形
polygons: null,
// 经纬度数组
points: map_data.points ?? [],
// 经纬度数据
points_data: null,
// 显示/隐藏 标记点
isShow: true,
// 显示/隐藏 对话框
dialogShow: false,
// 对话框按钮组
buttons: [{
text: '关闭'
}, {
text: '复制'
}],
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
let points = this.data.points
if (points.length > 0) {
points.forEach(item => {
item.latitude = Number(item.latitude)
item.longitude = Number(item.longitude)
})
let markers = points.map((item, index) => ({
id: index,
latitude: item.latitude,
longitude: item.longitude,
iconPath: this.data.Marker3_Activated,
width: 25,
height: 25
}));
this.setData({
markers,
points,
})
// 判断如果标记的点数大于2就给polygons赋值
if (points.length > 2) {
let polygons = [{
points,
fillColor: "#d5dff233", // 填充颜色:淡蓝色,7-8位为十六进制透明度00-FF
strokeColor: "#789cff", // 描边颜色:较深的淡蓝色
strokeWidth: 2, // 描边宽度
}]
this.setData({
polygons,
})
// let maxLo = 0
// let minLo = 180
// let maxLa = 0
// let minLa = 90
// points.forEach(item => {
// maxLo = Math.max(maxLo, item.longitude)
// minLo = Math.min(minLo, item.longitude)
// maxLa = Math.max(maxLa, item.latitude)
// minLa = Math.min(minLa, item.latitude)
// })
// console.log("地图中心点经度:", (maxLo + minLo) / 2);
// console.log("地图中心点纬度:", (maxLa + minLa) / 2);
}
this.includePoints()
}
},
/**
* 缩放视野以包含所有给定的坐标点
*/
includePoints() {
let points = this.data.points
this.mapCtx = wx.createMapContext('map')
this.mapCtx.includePoints({
padding: [10, 10, 10, 10],
points,
})
},
/**
* 绑定地图点击事件
*/
bindMap(e) {
let points = this.data.points // 不直接使用polygons[0].points是因为如果不够三个点会报错
let markers = this.data.markers // 点击地图添加一个标记点
let latitude = e.detail.latitude.toFixed(6)
let longitude = e.detail.longitude.toFixed(6)
points.push({
latitude,
longitude
})
let length = markers.length
markers.push({
id: length,
latitude,
longitude,
iconPath: this.data.Marker3_Activated,
width: 25,
height: 25
})
this.setData({
markers,
points
})
// 判断如果标记的点数大于2就给polygons赋值
if (points.length > 2) {
let polygons = [{
points,
fillColor: "#d5dff233", // 填充颜色:淡蓝色,7-8位为十六进制透明度00-FF
strokeColor: "#789cff", // 描边颜色:较深的淡蓝色
strokeWidth: 2, // 描边宽度
}]
this.setData({
polygons,
})
}
},
/**
* 清除 上一个标记点
*/
clearPrevious() {
let polygons = this.data.polygons
let markers = this.data.markers
let points = this.data.points
markers.pop()
points.pop()
if (markers.length < 3) {
polygons = null
} else {
polygons[0].points = points
}
this.setData({
polygons,
markers,
points
})
},
/**
* 清除 标记点和多边形
*/
clearGon() {
this.setData({
polygons: null,
markers: [],
points: []
})
},
/**
* 生成数据 按钮
*/
generate() {
let points = this.data.points
if (points.length > 2) {
let points_data = "points: " + JSON.stringify(points)
this.setData({
dialogShow: true,
points_data
})
} else {
wx.showToast({
title: '不能少于三个点',
icon: 'error'
})
}
},
/**
* 对话框 按钮
*/
dialogButton(e) {
this.setData({
dialogShow: false,
})
let choose = e.detail.item.text
if (choose == "复制") {
this.copy()
}
},
/**
* 复制 参数信息
*/
copy() {
wx.setClipboardData({
data: this.data.points_data,
})
},
/**
* 显示/隐藏标记点 按钮
*/
show() {
let isShow = this.data.isShow
let markers = this.data.markers
markers.forEach(item => {
// 标注的透明度 范围 0 ~ 1,对应 0% ~ 100%
item.alpha = isShow ? 0 : 1
})
this.setData({
isShow: !isShow,
markers
})
wx.showToast({
title: !isShow ? '显示标记点' : '隐藏标记点',
icon: 'none'
})
},
/**
* 跳转到 点与多边形 页面
*/
torange() {
let points = JSON.stringify(this.data.points)
wx.navigateTo({
url: '../range/range?points=' + points,
})
},
})
polygons.json
bash
{
"usingComponents": {
"mp-dialog": "weui-miniprogram/dialog/dialog"
},
"navigationBarTitleText": "地图多边形绘制"
}
polygons.wxml
bash
<!--pages/polygons/polygons.wxml-->
<map id="map" latitude="{{latitude}}" longitude="{{longitude}}" scale="{{scale}}" markers='{{markers}}' polygons="{{polygons}}" bindtap="bindMap">
<view class="control-btn" wx:if="{{points.length > 2}}">
<image src="{{Marker2_Activated}}" class="img" bindtap="torange"></image>
<view class="text">点与多边形</view>
</view>
</map>
<view style="display: flex; margin: 10px 0;">
<button type="default" bindtap="clearPrevious">清除上一个点</button>
<button type="warn" bindtap="clearGon">清除全部点面</button>
</view>
<view style="display: flex; margin: 10px 0;">
<button type="{{!isShow ? 'default' : 'warn' }}" bindtap="show">{{!isShow ? '显示' : '隐藏' }} 标记点</button>
<button style="height: 45px;" type="primary" bindtap="generate">生成数据</button>
</view>
<mp-dialog title="经纬度数组" show="{{dialogShow}}" bindbuttontap="dialogButton" buttons="{{buttons}}">
<view class="title">
<scroll-view scroll-y="true" class="scroll">
<text user-select="true">{{points_data}}</text>
</scroll-view>
</view>
</mp-dialog>
polygons.wxss
bash
/* pages/polygons/polygons.wxss */
page {
height: 100%;
}
map {
width: 100%;
height: calc(100% - 125px);
}
.title {
text-align: left;
margin-top: 10px;
}
.scroll {
height: 300px;
width: 100%;
margin-bottom: 15px;
}
.weui-dialog__btn_primary {
background-color: #07c160;
color: #fff;
}
.control-btn {
position: absolute;
width: 45px;
z-index: 99;
background: #FFF;
text-align: center;
border-radius: 4px;
right: 10px;
bottom: 40px;
}
.img {
height: 40px;
width: 40px;
}
.text {
font-size: small;
}
点与多边形关系
range.js
bash
// pages/range/range.js
import map_data from '@data/map_data'
Page({
/**
* 页面的初始数据
*/
data: {
// 地图中心点坐标
longitude: map_data.longitude,
latitude: map_data.latitude,
// 缩放级别
scale: map_data.scale,
// 标记点
markers: [],
// 多边形
polygons: null,
// 经纬度数组
points: [],
// 是否在多边形内部
isAtPolygons: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
let points = JSON.parse(options.points)
let polygons = [{
points,
fillColor: "#d5dff233", // 填充颜色:淡蓝色,7-8位为十六进制透明度00-FF
strokeColor: "#789cff", // 描边颜色:淡蓝色
strokeWidth: 2, // 描边宽度
}]
this.setData({
polygons,
points,
})
this.includePoints()
},
/**
* 缩放视野以包含所有给定的坐标点
*/
includePoints() {
this.mapCtx = wx.createMapContext('map')
this.mapCtx.includePoints({
padding: [10, 10, 10, 10],
points: this.data.points,
})
},
/**
* 绑定地图点击事件
*/
bindMap(e) {
let latitude = e.detail.latitude.toFixed(6)
let longitude = e.detail.longitude.toFixed(6)
let markers= [{
id: 1,
latitude,
longitude,
width: 25, // 默认图标的宽度
height: 34 // 默认图标的高度
}]
this.setData({
markers: markers,
})
let testPoint = {
latitude: latitude,
longitude: longitude
}
let polygon = this.data.points
let bool = this.isPointInPolygon(testPoint, polygon)
this.setData({
isAtPolygons: bool
})
},
/**
* 判断点是否在多边形内部
* @param {{longitude: number, latitude: number}} point - 待检测点
* @param {{longitude: number, latitude: number}[]} polygon - 多边形顶点数组
*/
isPointInPolygon(point, polygon) {
// 预处理:全部转为数值
const toNum = ({ longitude, latitude }) => ({
longitude: +longitude,
latitude: +latitude
});
point = toNum(point);
polygon = polygon.map(v => toNum(v));
// 检查顶点
for (const v of polygon) {
if (Math.abs(v.longitude - point.longitude) < 1e-9
&& Math.abs(v.latitude - point.latitude) < 1e-9) {
return true;
}
}
// 检查边
const n = polygon.length;
for (let i = 0; i < n; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % n];
if (this.isPointOnSegment(point, a, b)) return true;
}
// 射线法核心逻辑
let crossings = 0;
for (let i = 0; i < n; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % n];
const [aLat, bLat] = [a.latitude, b.latitude];
const pLat = point.latitude;
// 边跨越射线时才处理
if ((aLat >= pLat) === (bLat >= pLat)) continue;
// 排除水平边
if (aLat === bLat) continue;
// 计算交点经度
const t = (pLat - aLat) / (bLat - aLat);
const intersectLon = a.longitude + t * (b.longitude - a.longitude);
// 交点在射线右侧(经度更大)
if (intersectLon > point.longitude + 1e-9) {
crossings++;
}
}
return crossings % 2 === 1;
},
/**
* 判断点是否在多边形边上
* @param {{longitude: number, latitude: number}} p - 待检测点
* @param {{longitude: number, latitude: number}} a - 线段起点
* @param {{longitude: number, latitude: number}} b - 线段终点
*/
isPointOnSegment(p, a, b) {
// 强制转换为数值
const toNum = obj => ({
longitude: +obj.longitude,
latitude: +obj.latitude
});
p = toNum(p);
a = toNum(a);
b = toNum(b);
// 叉积判共线
const cross = (p.longitude - a.longitude) * (b.latitude - a.latitude)
- (p.latitude - a.latitude) * (b.longitude - a.longitude);
if (Math.abs(cross) > 1e-9) return false;
// 包围盒检查
const minLon = Math.min(a.longitude, b.longitude);
const maxLon = Math.max(a.longitude, b.longitude);
const minLat = Math.min(a.latitude, b.latitude);
const maxLat = Math.max(a.latitude, b.latitude);
return p.longitude >= minLon - 1e-9
&& p.longitude <= maxLon + 1e-9
&& p.latitude >= minLat - 1e-9
&& p.latitude <= maxLat + 1e-9;
},
})
range.json
bash
{
"usingComponents": {},
"navigationBarTitleText": "点与多边形关系"
}
range.wxml
bash
<!--pages/range/range.wxml-->
<map id="map" latitude="{{latitude}}" longitude="{{longitude}}" scale="{{scale}}" markers='{{markers}}' polygons="{{polygons}}" bindtap="bindMap" />
<view class="text">
点
<span style="color: {{isAtPolygons ? 'black' : 'red'}}; margin: 0 5px;">
{{isAtPolygons ? '在' : '不在'}}
</span>
多边形内
</view>
range.wxss
bash
/* pages/range/range.wxss */
page{
height: 100%;
}
map {
width: 100%;
height: calc(100% - 100px);
}
.text {
display: flex;
justify-content: center;
align-items: center;
font-size: x-large;
margin: 5px;
}
射线法逻辑的代码使用deepseek辅助生成