299 lines
7.7 KiB
JavaScript
299 lines
7.7 KiB
JavaScript
|
|
/**
|
|||
|
|
* 电子围栏坐标点模型
|
|||
|
|
* 用于存储围栏绘制过程中用户选定的经纬度坐标点
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const { DataTypes } = require('sequelize');
|
|||
|
|
const { sequelize } = require('../config/database-simple');
|
|||
|
|
const BaseModel = require('./BaseModel');
|
|||
|
|
|
|||
|
|
class ElectronicFencePoint extends BaseModel {
|
|||
|
|
// 模型属性定义
|
|||
|
|
static attributes = {
|
|||
|
|
id: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
primaryKey: true,
|
|||
|
|
autoIncrement: true,
|
|||
|
|
comment: '主键ID'
|
|||
|
|
},
|
|||
|
|
fence_id: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: false,
|
|||
|
|
comment: '关联的围栏ID',
|
|||
|
|
references: {
|
|||
|
|
model: 'electronic_fences',
|
|||
|
|
key: 'id'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
point_order: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: false,
|
|||
|
|
comment: '坐标点在围栏中的顺序(从0开始)'
|
|||
|
|
},
|
|||
|
|
longitude: {
|
|||
|
|
type: DataTypes.DECIMAL(10, 7),
|
|||
|
|
allowNull: false,
|
|||
|
|
comment: '经度'
|
|||
|
|
},
|
|||
|
|
latitude: {
|
|||
|
|
type: DataTypes.DECIMAL(10, 7),
|
|||
|
|
allowNull: false,
|
|||
|
|
comment: '纬度'
|
|||
|
|
},
|
|||
|
|
point_type: {
|
|||
|
|
type: DataTypes.ENUM('corner', 'control', 'marker'),
|
|||
|
|
allowNull: false,
|
|||
|
|
defaultValue: 'corner',
|
|||
|
|
comment: '坐标点类型:corner-拐角点,control-控制点,marker-标记点'
|
|||
|
|
},
|
|||
|
|
description: {
|
|||
|
|
type: DataTypes.TEXT,
|
|||
|
|
allowNull: true,
|
|||
|
|
comment: '坐标点描述信息'
|
|||
|
|
},
|
|||
|
|
is_active: {
|
|||
|
|
type: DataTypes.BOOLEAN,
|
|||
|
|
allowNull: false,
|
|||
|
|
defaultValue: true,
|
|||
|
|
comment: '是否激活'
|
|||
|
|
},
|
|||
|
|
created_by: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: true,
|
|||
|
|
comment: '创建人ID'
|
|||
|
|
},
|
|||
|
|
updated_by: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: true,
|
|||
|
|
comment: '更新人ID'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 模型选项
|
|||
|
|
static options = {
|
|||
|
|
tableName: 'electronic_fence_points',
|
|||
|
|
comment: '电子围栏坐标点表',
|
|||
|
|
indexes: [
|
|||
|
|
{
|
|||
|
|
fields: ['fence_id']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
fields: ['fence_id', 'point_order']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
fields: ['longitude', 'latitude']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
fields: ['point_type']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
fields: ['is_active']
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
hooks: {
|
|||
|
|
beforeCreate: (point, options) => {
|
|||
|
|
// 创建前钩子
|
|||
|
|
if (!point.point_order && point.point_order !== 0) {
|
|||
|
|
// 如果没有指定顺序,自动计算
|
|||
|
|
return ElectronicFencePoint.count({
|
|||
|
|
where: { fence_id: point.fence_id }
|
|||
|
|
}).then(count => {
|
|||
|
|
point.point_order = count;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 实例方法
|
|||
|
|
/**
|
|||
|
|
* 获取坐标点的经纬度对象
|
|||
|
|
* @returns {Object} 包含lng和lat的对象
|
|||
|
|
*/
|
|||
|
|
getCoordinates() {
|
|||
|
|
return {
|
|||
|
|
lng: parseFloat(this.longitude),
|
|||
|
|
lat: parseFloat(this.latitude)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置坐标点
|
|||
|
|
* @param {number} lng - 经度
|
|||
|
|
* @param {number} lat - 纬度
|
|||
|
|
*/
|
|||
|
|
setCoordinates(lng, lat) {
|
|||
|
|
this.longitude = lng;
|
|||
|
|
this.latitude = lat;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 转换为前端格式
|
|||
|
|
* @returns {Object} 前端使用的坐标点格式
|
|||
|
|
*/
|
|||
|
|
toFrontendFormat() {
|
|||
|
|
return {
|
|||
|
|
id: this.id,
|
|||
|
|
fenceId: this.fence_id,
|
|||
|
|
pointOrder: this.point_order,
|
|||
|
|
lng: parseFloat(this.longitude),
|
|||
|
|
lat: parseFloat(this.latitude),
|
|||
|
|
pointType: this.point_type,
|
|||
|
|
description: this.description,
|
|||
|
|
isActive: this.is_active,
|
|||
|
|
createdAt: this.created_at,
|
|||
|
|
updatedAt: this.updated_at
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算到另一个点的距离(米)
|
|||
|
|
* @param {ElectronicFencePoint} otherPoint - 另一个坐标点
|
|||
|
|
* @returns {number} 距离(米)
|
|||
|
|
*/
|
|||
|
|
distanceTo(otherPoint) {
|
|||
|
|
const R = 6371000; // 地球半径(米)
|
|||
|
|
const lat1 = this.latitude * Math.PI / 180;
|
|||
|
|
const lat2 = otherPoint.latitude * Math.PI / 180;
|
|||
|
|
const deltaLat = (otherPoint.latitude - this.latitude) * Math.PI / 180;
|
|||
|
|
const deltaLng = (otherPoint.longitude - this.longitude) * Math.PI / 180;
|
|||
|
|
|
|||
|
|
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
|||
|
|
Math.cos(lat1) * Math.cos(lat2) *
|
|||
|
|
Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
|
|||
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|||
|
|
|
|||
|
|
return R * c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查坐标点是否在指定范围内
|
|||
|
|
* @param {number} centerLng - 中心点经度
|
|||
|
|
* @param {number} centerLat - 中心点纬度
|
|||
|
|
* @param {number} radius - 半径(米)
|
|||
|
|
* @returns {boolean} 是否在范围内
|
|||
|
|
*/
|
|||
|
|
isWithinRadius(centerLng, centerLat, radius) {
|
|||
|
|
const centerPoint = {
|
|||
|
|
longitude: centerLng,
|
|||
|
|
latitude: centerLat
|
|||
|
|
};
|
|||
|
|
return this.distanceTo(centerPoint) <= radius;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 静态方法
|
|||
|
|
/**
|
|||
|
|
* 根据围栏ID获取所有坐标点
|
|||
|
|
* @param {number} fenceId - 围栏ID
|
|||
|
|
* @param {Object} options - 查询选项
|
|||
|
|
* @returns {Promise<Array>} 坐标点数组
|
|||
|
|
*/
|
|||
|
|
static async getByFenceId(fenceId, options = {}) {
|
|||
|
|
const defaultOptions = {
|
|||
|
|
where: {
|
|||
|
|
fence_id: fenceId,
|
|||
|
|
is_active: true
|
|||
|
|
},
|
|||
|
|
order: [['point_order', 'ASC']]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return this.findAll({
|
|||
|
|
...defaultOptions,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量创建坐标点
|
|||
|
|
* @param {number} fenceId - 围栏ID
|
|||
|
|
* @param {Array} points - 坐标点数组
|
|||
|
|
* @param {Object} options - 创建选项
|
|||
|
|
* @returns {Promise<Array>} 创建的坐标点数组
|
|||
|
|
*/
|
|||
|
|
static async createPoints(fenceId, points, options = {}) {
|
|||
|
|
const pointsData = points.map((point, index) => ({
|
|||
|
|
fence_id: fenceId,
|
|||
|
|
point_order: index,
|
|||
|
|
longitude: point.lng,
|
|||
|
|
latitude: point.lat,
|
|||
|
|
point_type: point.type || 'corner',
|
|||
|
|
description: point.description || null,
|
|||
|
|
created_by: options.createdBy || null
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
return this.bulkCreate(pointsData, options);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新围栏的所有坐标点
|
|||
|
|
* @param {number} fenceId - 围栏ID
|
|||
|
|
* @param {Array} points - 新的坐标点数组
|
|||
|
|
* @param {Object} options - 更新选项
|
|||
|
|
* @returns {Promise<Array>} 更新后的坐标点数组
|
|||
|
|
*/
|
|||
|
|
static async updateFencePoints(fenceId, points, options = {}) {
|
|||
|
|
const transaction = options.transaction || await sequelize.transaction();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 删除现有坐标点
|
|||
|
|
await this.destroy({
|
|||
|
|
where: { fence_id: fenceId },
|
|||
|
|
transaction
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 创建新的坐标点
|
|||
|
|
const newPoints = await this.createPoints(fenceId, points, {
|
|||
|
|
...options,
|
|||
|
|
transaction
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!options.transaction) {
|
|||
|
|
await transaction.commit();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return newPoints;
|
|||
|
|
} catch (error) {
|
|||
|
|
if (!options.transaction) {
|
|||
|
|
await transaction.rollback();
|
|||
|
|
}
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取围栏的边界框
|
|||
|
|
* @param {number} fenceId - 围栏ID
|
|||
|
|
* @returns {Promise<Object>} 边界框对象
|
|||
|
|
*/
|
|||
|
|
static async getFenceBounds(fenceId) {
|
|||
|
|
const points = await this.getByFenceId(fenceId);
|
|||
|
|
|
|||
|
|
if (points.length === 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lngs = points.map(p => parseFloat(p.longitude));
|
|||
|
|
const lats = points.map(p => parseFloat(p.latitude));
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
minLng: Math.min(...lngs),
|
|||
|
|
maxLng: Math.max(...lngs),
|
|||
|
|
minLat: Math.min(...lats),
|
|||
|
|
maxLat: Math.max(...lats),
|
|||
|
|
center: {
|
|||
|
|
lng: (Math.min(...lngs) + Math.max(...lngs)) / 2,
|
|||
|
|
lat: (Math.min(...lats) + Math.max(...lats)) / 2
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化模型
|
|||
|
|
ElectronicFencePoint.init(ElectronicFencePoint.attributes, {
|
|||
|
|
...ElectronicFencePoint.options,
|
|||
|
|
sequelize,
|
|||
|
|
modelName: 'ElectronicFencePoint'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
module.exports = ElectronicFencePoint;
|