射线法实现地理电子围栏
本文文字及代码均使用AI生成。
地图电子围栏实现方案
下面是一个基于 HTML、JavaScript 和 Canvas 的电子围栏模拟系统,实现了多边形围栏绘制和坐标点判断功能。系统使用射线法 (Ray Casting Algorithm) 来判断点是否在多边形内部,这是一种高效且常用的地理空间判断算法,效果图如下:

在线演示:[ 点击这里 ]
1. 射线法(Ray Casting Algorithm)的原理
射线法是判断点是否在多边形内部的经典算法,其核心原理如下:
- 发射射线:从目标点向右(或任意固定方向)发射一条水平射线。
- 统计交点:计算该射线与多边形所有边的交点数量。
- 判断规则:
- 若交点数量为奇数,则点在多边形内部;
- 若交点数量为偶数(包括 0),则点在多边形外部。
核心逻辑:射线每穿过多边形的一条边,就会从 “内部” 和 “外部” 状态切换一次,最终状态由切换次数的奇偶性决定。该算法对简单多边形(非自相交)和复杂多边形均适用,且实现简单、效率较高。
2. 其它判断点是否在多边形内部的方法
除射线法外,常见的方法还有:
(1)转角法(Winding Number Algorithm)
- 原理:计算从目标点到多边形所有顶点的连线与正 x 轴的夹角变化总和(即 “绕数”)。
- 判断规则:
- 若绕数为0,点在多边形外部;
- 若绕数为非 0,点在多边形内部。
- 特点:能区分点在多边形内部还是外部,且对自相交多边形也能处理,但计算量略大于射线法。
(2)叉积法(适用于凸多边形)
- 原理:对于凸多边形,判断点是否在所有边的 “内侧”(通过叉积判断方向一致性)。
- 判断规则:若点在多边形所有边的同一侧(如左侧),则在内部;否则在外部。
- 特点:仅适用于凸多边形,计算速度快(时间复杂度 O (n),n 为边数)。
(3)扫描线算法
- 原理:通过扫描线与多边形边的交点,记录 “进入” 和 “退出” 状态,判断点所在区间的状态。
- 特点:适合批量点判断,常与空间索引结合优化性能。
3. 电子围栏的性能优化方法
电子围栏的性能优化需结合场景(如围栏数量、点数量、实时性要求),常见手段如下:
(1)空间索引优化
- 原理:通过预处理将围栏或点划分到不同的空间区域,减少判断时的计算量。
- 常用索引:
- 网格索引:将地图划分为固定大小的网格,每个网格关联包含的围栏,判断点时仅需检查所在网格的围栏。
- R 树 / R + 树:适用于不规则分布的围栏,高效检索与目标点可能相交的围栏。
(2)围栏预处理
- 简化多边形:对顶点过多的围栏,使用道格拉斯 - 普克算法(Douglas-Peucker)简化顶点,减少判断时的边数量。
- 合并围栏:对重叠或相邻的围栏进行合并,减少整体围栏数量。
(3)算法优化
- 提前过滤:先通过围栏的外接矩形(Bounding Box)快速判断点是否可能在围栏内,若不在则直接排除(无需执行完整的射线法 / 转角法)。
- 批量处理:对多个点同时判断时,采用并行计算或向量运算加速。
(4)缓存与更新策略
- 缓存结果:对频繁查询的固定点,缓存其是否在围栏内的结果,避免重复计算。
- 增量更新:围栏或点集变化时,仅重新计算受影响的部分,而非全量重算。
(5)硬件与工程优化
- Web 场景:使用 WebWorker 处理密集计算,避免阻塞主线程;对大规模数据,考虑使用 WebGL 加速图形渲染和判断。
- 后端场景:采用 C++ 等高性能语言实现核心算法,或使用 GPU 加速并行计算。
这些方法可根据实际需求组合使用,例如:先用外接矩形快速过滤,再用射线法精确判断,同时结合网格索引减少待检查的围栏数量,大幅提升电子围栏系统的响应速度。
源码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电子围栏模拟系统(优化版)</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f0f0f0;
}
.container {
display: flex;
gap: 20px;
}
#mapCanvas {
border: 2px solid #333;
background-color: white;
cursor: crosshair;
}
.controls {
background-color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
width: 300px;
}
.control-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, button {
width: 100%;
padding: 8px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#result {
margin-top: 15px;
padding: 10px;
border-radius: 3px;
}
.inside {
background-color: #dff0d8;
color: #3c763d;
}
.outside {
background-color: #f2dede;
color: #a94442;
}
.fence-list {
max-height: 150px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 5px;
margin-bottom: 10px;
}
.fence-item {
padding: 5px;
margin-bottom: 5px;
background-color: #f9f9f9;
border-radius: 3px;
}
.magnet-hint {
position: absolute;
background-color: rgba(255, 152, 0, 0.8);
color: white;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
pointer-events: none;
display: none;
}
</style>
</head>
<body>
<h1>电子围栏模拟系统(优化版)</h1>
<div class="container">
<div style="position: relative;">
<canvas id="mapCanvas" width="800" height="600"></canvas>
<div class="magnet-hint" id="magnetHint">吸附闭合</div>
</div>
<div class="controls">
<div class="control-group">
<label>围栏操作</label>
<button id="startFence">开始绘制围栏</button>
<button id="finishFence" disabled>完成当前围栏</button>
<button id="clearAll">清除所有围栏</button>
</div>
<div class="control-group">
<label>围栏列表</label>
<div id="fenceList" class="fence-list"></div>
</div>
<div class="control-group">
<label>坐标检查</label>
<input type="number" id="xCoord" placeholder="X坐标" min="0" max="800">
<input type="number" id="yCoord" placeholder="Y坐标" min="0" max="600">
<button id="checkPoint">检查坐标</button>
</div>
<div id="result" class="outside">
检查结果将显示在这里
</div>
</div>
</div>
<script>
// 获取画布和上下文
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
const magnetHint = document.getElementById('magnetHint');
// 围栏数据
let fences = []; // 所有围栏
let currentFence = []; // 当前正在绘制的围栏
let isDrawing = false; // 是否正在绘制
const MAGNET_DISTANCE = 20; // 磁吸生效距离(像素)
// DOM元素
const startFenceBtn = document.getElementById('startFence');
const finishFenceBtn = document.getElementById('finishFence');
const clearAllBtn = document.getElementById('clearAll');
const checkPointBtn = document.getElementById('checkPoint');
const xCoordInput = document.getElementById('xCoord');
const yCoordInput = document.getElementById('yCoord');
const resultDiv = document.getElementById('result');
const fenceListDiv = document.getElementById('fenceList');
// 初始化画布
function initCanvas() {
ctx.fillStyle = '#e8f4f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格背景
drawGrid();
}
// 绘制网格
function drawGrid() {
ctx.strokeStyle = '#d0e8f0';
ctx.lineWidth = 1;
// 绘制水平线
for (let y = 50; y < canvas.height; y += 50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
// 绘制坐标
ctx.fillStyle = '#666';
ctx.font = '10px Arial';
ctx.fillText(y, 5, y + 3);
}
// 绘制垂直线
for (let x = 50; x < canvas.width; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
// 绘制坐标
ctx.fillStyle = '#666';
ctx.font = '10px Arial';
ctx.fillText(x, x - 5, 15);
}
}
// 计算两点之间的距离
function getDistance(p1, p2) {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
}
// 射线法判断点是否在多边形内部
function isPointInPolygon(point, polygon) {
const x = point.x; // 目标点的X坐标
const y = point.y; // 目标点的Y坐标
let inside = false; // 标记点是否在多边形内部,初始为外部
// 遍历多边形的每条边:
// i是当前顶点索引,j是前一个顶点索引(形成边 j->i)
// 循环结束后j会自动更新为i,i递增,实现边的依次遍历
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
// 获取当前边的两个顶点坐标
const xi = polygon[i].x, yi = polygon[i].y; // 当前顶点(i)的坐标
const xj = polygon[j].x, yj = polygon[j].y; // 前一个顶点(j)的坐标
// 核心判断逻辑:检查射线是否与当前边相交
// 分为两部分:垂直线范围检查 + 水平射线交点检查
// 1. 垂直线范围检查:((yi > y) !== (yj > y))
// 作用:判断点的Y坐标是否在当前边的Y坐标区间内
// 原理:
// - 如果边的两个端点(yi和yj)分别在点y坐标的两侧(一个大于y,一个小于y)
// - 则说明点的水平射线(y固定)可能与这条边相交
// - 若两点都在y上方或都在y下方,则射线不可能与边相交,直接排除
const isYRangeValid = (yi > y) !== (yj > y);
// 2. 水平射线交点检查:x < (xj - xi) * (y - yi) / (yj - yi) + xi
// 作用:计算射线与边的交点X坐标,并判断是否在点的右侧
// 原理:
// - 公式是通过直线方程推导的:求边所在直线与y=point.y的交点X坐标
// - 若交点X坐标大于点的X坐标,说明射线(向右延伸)会穿过这条边
const intersectionX = (xj - xi) * (y - yi) / (yj - yi) + xi;
const isIntersectionRight = x < intersectionX;
// 当两个条件同时满足时,射线与当前边相交
const isIntersect = isYRangeValid && isIntersectionRight;
// 每相交一次,内外状态翻转一次
if (isIntersect) {
inside = !inside;
}
}
// 最终返回点是否在内部
return inside;
}
// 检查点是否在任何围栏内
function checkPointInAnyFence(x, y) {
const point = {x, y};
for (let i = 0; i < fences.length; i++) {
if (isPointInPolygon(point, fences[i])) {
return { inside: true, fenceIndex: i };
}
}
return { inside: false };
}
// 绘制所有围栏
function drawFences() {
fences.forEach((fence, index) => {
drawPolygon(fence, index === fences.length - 1 ? '#4CAF50' : '#2196F3');
});
// 绘制当前正在绘制的围栏
if (currentFence.length > 0) {
drawCurrentFence();
}
}
// 专门绘制当前正在编辑的围栏
function drawCurrentFence() {
// 绘制所有已有点
currentFence.forEach(point => {
drawVertex(point.x, point.y);
});
// 如果有多个点,绘制连接线
if (currentFence.length > 1) {
ctx.beginPath();
ctx.moveTo(currentFence[0].x, currentFence[0].y);
for (let i = 1; i < currentFence.length; i++) {
ctx.lineTo(currentFence[i].x, currentFence[i].y);
}
// 绘制边框
ctx.strokeStyle = '#FF9800';
ctx.lineWidth = 2;
ctx.stroke();
}
// 绘制磁吸范围指示器(如果满足条件)
if (currentFence.length > 1) {
const firstPoint = currentFence[0];
const lastPoint = currentFence[currentFence.length - 1];
// 如果最后一个点靠近第一个点,绘制磁吸范围
if (getDistance(lastPoint, firstPoint) < MAGNET_DISTANCE) {
ctx.strokeStyle = 'rgba(255, 152, 0, 0.5)';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.beginPath();
ctx.arc(firstPoint.x, firstPoint.y, MAGNET_DISTANCE, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
// 绘制多边形(已完成的围栏)
function drawPolygon(points, color, fill = true) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
// 闭合多边形
if (fill && points.length >= 3) {
ctx.closePath();
ctx.fillStyle = color.replace(')', ', 0.2)').replace('rgb', 'rgba');
ctx.fill();
}
// 绘制边框
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// 绘制顶点
points.forEach(point => {
drawVertex(point.x, point.y, color);
});
}
// 绘制顶点(单独提取方法,确保第一个点也能显示)
function drawVertex(x, y, color = '#FF9800') {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}
// 绘制检查点
function drawPoint(x, y, color = '#ff0000') {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
// 更新围栏列表显示
function updateFenceList() {
fenceListDiv.innerHTML = '';
fences.forEach((fence, index) => {
const fenceItem = document.createElement('div');
fenceItem.className = 'fence-item';
fenceItem.innerHTML = `
围栏 ${index + 1} (${fence.length} 个顶点)
<button onclick="removeFence(${index})" style="width: auto; padding: 2px 5px; float: right; background-color: #f44336;">删除</button>
`;
fenceListDiv.appendChild(fenceItem);
});
}
// 移除指定围栏
function removeFence(index) {
fences.splice(index, 1);
redraw();
updateFenceList();
}
// 重绘整个画布
function redraw() {
initCanvas();
drawFences();
}
// 处理磁吸逻辑
function handleMagnetism(x, y) {
// 只有当已有至少一个点时才可能触发磁吸
if (currentFence.length === 0) {
return {x, y, magnetized: false};
}
const firstPoint = currentFence[0];
const distance = getDistance({x, y}, firstPoint);
// 如果在磁吸范围内,返回第一个点的坐标
if (distance < MAGNET_DISTANCE) {
return {
x: firstPoint.x,
y: firstPoint.y,
magnetized: true
};
}
return {x, y, magnetized: false};
}
// 显示/隐藏磁吸提示
function showMagnetHint(show, x, y) {
if (show) {
magnetHint.style.display = 'block';
magnetHint.style.left = `${x + 10}px`;
magnetHint.style.top = `${y - 20}px`;
} else {
magnetHint.style.display = 'none';
}
}
// 事件监听 - 画布点击
canvas.addEventListener('click', (e) => {
if (!isDrawing) return;
// 获取相对于画布的坐标
const rect = canvas.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
// 应用磁吸效果(但第一个点不磁吸)
const magnetResult = currentFence.length > 0 ? handleMagnetism(x, y) : {x, y, magnetized: false};
x = magnetResult.x;
y = magnetResult.y;
// 添加点到当前围栏
currentFence.push({x, y});
// 启用完成按钮(至少有一个点就可以点击完成,但实际完成需要3个点)
finishFenceBtn.disabled = false;
// 如果触发了磁吸,自动完成围栏绘制
if (magnetResult.magnetized && currentFence.length >= 3) {
// 移除最后一个点(重复的第一个点),保持多边形闭合但不重复存储
currentFence.pop();
fences.push([...currentFence]);
currentFence = [];
isDrawing = false;
startFenceBtn.disabled = false;
finishFenceBtn.disabled = true;
showMagnetHint(false);
alert('围栏已通过磁吸自动闭合');
}
// 更新输入框
xCoordInput.value = Math.round(x);
yCoordInput.value = Math.round(y);
// 重绘
redraw();
updateFenceList();
});
// 事件监听 - 鼠标移动(显示磁吸提示)
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing || currentFence.length < 1) {
showMagnetHint(false);
return;
}
// 获取相对于画布的坐标
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否在磁吸范围内
const firstPoint = currentFence[0];
const distance = getDistance({x, y}, firstPoint);
const inMagnetRange = distance < MAGNET_DISTANCE;
// 显示或隐藏磁吸提示
showMagnetHint(inMagnetRange, e.clientX, e.clientY);
});
// 事件监听 - 开始绘制围栏
startFenceBtn.addEventListener('click', () => {
isDrawing = true;
currentFence = [];
startFenceBtn.disabled = true;
finishFenceBtn.disabled = true; // 开始时禁用完成按钮,直到有第一个点
alert('点击画布添加围栏顶点,第一个点将立即显示,当最后一个点靠近第一个点时会自动吸附闭合');
});
// 事件监听 - 完成当前围栏
finishFenceBtn.addEventListener('click', () => {
if (currentFence.length >= 3) {
// 检查是否需要闭合(最后一个点是否与第一个点重合)
const firstPoint = currentFence[0];
const lastPoint = currentFence[currentFence.length - 1];
if (getDistance(firstPoint, lastPoint) > 1) {
// 如果不重合,添加第一个点作为最后一个点使其闭合
currentFence.push(firstPoint);
}
fences.push([...currentFence]);
currentFence = [];
isDrawing = false;
startFenceBtn.disabled = false;
finishFenceBtn.disabled = true;
redraw();
updateFenceList();
} else {
alert('围栏至少需要3个顶点才能完成');
}
});
// 事件监听 - 清除所有围栏
clearAllBtn.addEventListener('click', () => {
if (confirm('确定要清除所有围栏吗?')) {
fences = [];
currentFence = [];
isDrawing = false;
startFenceBtn.disabled = false;
finishFenceBtn.disabled = true;
redraw();
updateFenceList();
}
});
// 事件监听 - 检查点
checkPointBtn.addEventListener('click', () => {
const x = parseFloat(xCoordInput.value);
const y = parseFloat(yCoordInput.value);
if (isNaN(x) || isNaN(y) || x < 0 || x > canvas.width || y < 0 || y > canvas.height) {
alert('请输入有效的坐标值 (0-' + canvas.width + ', 0-' + canvas.height + ')');
return;
}
// 重绘以清除之前的检查点
redraw();
// 绘制检查点
drawPoint(x, y);
// 检查是否在围栏内
const result = checkPointInAnyFence(x, y);
// 显示结果
if (result.inside) {
resultDiv.textContent = `坐标 (${x}, ${y}) 在 围栏 ${result.fenceIndex + 1} 内部`;
resultDiv.className = 'inside';
} else {
resultDiv.textContent = `坐标 (${x}, ${y}) 在所有围栏外部`;
resultDiv.className = 'outside';
}
});
// 初始化
initCanvas();
</script>
</body>
</html>