@fleet-frontend/mower-maps 0.1.0-beta.9 → 0.1.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3934,336 +3934,6 @@ class BoundaryLayer extends BaseLayer {
3934
3934
  }
3935
3935
  }
3936
3936
 
3937
- /**
3938
- * 障碍物图层
3939
- * 专门处理障碍物元素的渲染
3940
- */
3941
- class ObstacleLayer extends BaseLayer {
3942
- constructor() {
3943
- super();
3944
- this.level = 5;
3945
- this.type = LAYER_DEFAULT_TYPE.OBSTACLE;
3946
- }
3947
- /**
3948
- * SVG渲染方法
3949
- */
3950
- drawSVG(svgGroup) {
3951
- if (!this.visible || this.elements.length === 0) {
3952
- return;
3953
- }
3954
- // 只渲染障碍物类型的元素
3955
- for (const element of this.elements) {
3956
- if (element.type === 'obstacle') {
3957
- this.renderObstacle(svgGroup, element);
3958
- }
3959
- }
3960
- }
3961
- /**
3962
- * 渲染障碍物元素
3963
- */
3964
- renderObstacle(svgGroup, element) {
3965
- const { coordinates, style } = element;
3966
- if (coordinates.length < 3)
3967
- return;
3968
- const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
3969
- // 构建点集合,使用整数坐标
3970
- const points = coordinates.map((coord) => `${coord[0]},${coord[1]}`).join(' ');
3971
- polygon.setAttribute('points', points);
3972
- polygon.setAttribute('fill', style.fillColor || 'rgba(220, 53, 69, 0.2)');
3973
- polygon.setAttribute('stroke', style.lineColor || '#dc3545');
3974
- // 确保最小线条宽度
3975
- const lineWidth = Math.max(style.lineWidth || 2, 0.5);
3976
- polygon.setAttribute('stroke-width', lineWidth.toString());
3977
- polygon.setAttribute('stroke-linecap', 'round');
3978
- polygon.setAttribute('stroke-linejoin', 'round');
3979
- polygon.setAttribute('opacity', (style.opacity || 1).toString());
3980
- polygon.setAttribute('vector-effect', 'non-scaling-stroke');
3981
- polygon.classList.add('vector-obstacle');
3982
- svgGroup.appendChild(polygon);
3983
- }
3984
- }
3985
-
3986
- var chargingPileImage = "data:image/png;base64,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";
3987
-
3988
- /**
3989
- * 充电桩图层
3990
- * 专门处理充电桩元素的渲染
3991
- */
3992
- class ChargingPileLayer extends BaseLayer {
3993
- constructor() {
3994
- super();
3995
- this.level = 8; // 中等层级
3996
- this.type = LAYER_DEFAULT_TYPE.CHARGING_PILE;
3997
- }
3998
- /**
3999
- * SVG渲染方法
4000
- */
4001
- drawSVG(svgGroup) {
4002
- if (!this.visible || this.elements.length === 0) {
4003
- return;
4004
- }
4005
- // 只渲染充电桩类型的元素
4006
- for (const element of this.elements) {
4007
- if (element.type === 'charging_pile') {
4008
- this.renderChargingPile(svgGroup, element);
4009
- }
4010
- }
4011
- }
4012
- /**
4013
- * 渲染充电桩元素
4014
- */
4015
- renderChargingPile(svgGroup, element) {
4016
- const { coordinates, style } = element;
4017
- if (coordinates.length === 0)
4018
- return;
4019
- const center = coordinates[0];
4020
- const size = style.radius ? style.radius * 2 : 55; // 默认55px大小
4021
- const direction = element.originalData?.direction || 0;
4022
- // 将弧度转换为角度
4023
- const angle = (direction * 180) / Math.PI;
4024
- const rotationDegree = 270 - angle; // 坐标系转换
4025
- const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
4026
- // 图片居中定位
4027
- const x = center[0];
4028
- const y = center[1];
4029
- image.setAttribute('x', x.toString());
4030
- image.setAttribute('y', y.toString());
4031
- image.setAttribute('width', `${size}px`);
4032
- image.setAttribute('height', `${size}px`);
4033
- image.setAttribute('href', chargingPileImage);
4034
- image.setAttribute('opacity', '0'); // 初始透明
4035
- // 添加SVG原生动画,传入默认角度
4036
- this.addChargingPileAnimation(image, center, rotationDegree);
4037
- image.classList.add('vector-charging-pile');
4038
- svgGroup.appendChild(image);
4039
- }
4040
- /**
4041
- * 添加充电桩SVG原生动画
4042
- */
4043
- addChargingPileAnimation(image, center, defaultAngle) {
4044
- // 透明度动画 - 先显示出来
4045
- const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
4046
- animateOpacity.setAttribute('attributeName', 'opacity');
4047
- animateOpacity.setAttribute('values', '0;1');
4048
- animateOpacity.setAttribute('dur', '0.5s');
4049
- animateOpacity.setAttribute('fill', 'freeze');
4050
- image.appendChild(animateOpacity);
4051
- // 旋转动画 - 从180度旋转到默认角度
4052
- const animateTransform = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
4053
- animateTransform.setAttribute('attributeName', 'transform');
4054
- animateTransform.setAttribute('type', 'rotate');
4055
- animateTransform.setAttribute('values', `${180 + defaultAngle} ${center[0]} ${center[1]};${defaultAngle} ${center[0]} ${center[1]}`);
4056
- animateTransform.setAttribute('dur', '1s');
4057
- animateTransform.setAttribute('repeatCount', '1'); // 只播放一次
4058
- animateTransform.setAttribute('begin', '0.5s'); // 延迟0.5秒开始,等透明度动画完成
4059
- animateTransform.setAttribute('fill', 'freeze'); // 保持最终状态
4060
- image.appendChild(animateTransform);
4061
- }
4062
- }
4063
-
4064
- /**
4065
- * 点图层
4066
- * 专门处理点元素的渲染
4067
- */
4068
- class PointLayer extends BaseLayer {
4069
- constructor() {
4070
- super();
4071
- this.level = 11;
4072
- this.type = LAYER_DEFAULT_TYPE.POINT;
4073
- }
4074
- /**
4075
- * SVG渲染方法
4076
- */
4077
- drawSVG(svgGroup) {
4078
- if (!this.visible || this.elements.length === 0) {
4079
- return;
4080
- }
4081
- // 只渲染点类型的元素
4082
- for (const element of this.elements) {
4083
- if (element.type === 'point') {
4084
- this.renderPoint(svgGroup, element);
4085
- }
4086
- }
4087
- }
4088
- /**
4089
- * 渲染点元素
4090
- */
4091
- renderPoint(svgGroup, element) {
4092
- const { coordinates, style } = element;
4093
- if (coordinates.length === 0)
4094
- return;
4095
- const coordinate = coordinates[0];
4096
- const radius = style.radius || 4;
4097
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4098
- circle.setAttribute('cx', coordinate[0].toString());
4099
- circle.setAttribute('cy', coordinate[1].toString());
4100
- circle.setAttribute('r', radius.toString());
4101
- circle.setAttribute('fill', style.fillColor || style.strokeColor || '#000000');
4102
- circle.setAttribute('stroke', style.strokeColor || '#000000');
4103
- // 确保最小线条宽度
4104
- const lineWidth = Math.max(style.lineWidth || 1, 0.5);
4105
- circle.setAttribute('stroke-width', lineWidth.toString());
4106
- circle.setAttribute('opacity', (style.opacity || 1).toString());
4107
- circle.setAttribute('vector-effect', 'non-scaling-stroke');
4108
- circle.classList.add('vector-point');
4109
- svgGroup.appendChild(circle);
4110
- }
4111
- }
4112
-
4113
- /**
4114
- * SVG元素图层
4115
- * 专门处理SVG元素的渲染
4116
- */
4117
- class SvgElementLayer extends BaseLayer {
4118
- constructor() {
4119
- super();
4120
- this.level = 6;
4121
- this.scale = 1;
4122
- this.type = LAYER_DEFAULT_TYPE.SVG;
4123
- }
4124
- /**
4125
- * SVG渲染方法
4126
- */
4127
- drawSVG(svgGroup) {
4128
- if (!this.visible || this.elements.length === 0) {
4129
- return;
4130
- }
4131
- // 只渲染SVG类型的元素
4132
- for (const element of this.elements) {
4133
- const expirationTs = element.originalData?.expiration_ts;
4134
- const current = Date.now() / 1000;
4135
- if (expirationTs && current > expirationTs) {
4136
- continue;
4137
- }
4138
- if (element.type === 'svg') {
4139
- this.renderSvgElement(svgGroup, element);
4140
- }
4141
- }
4142
- }
4143
- /**
4144
- * 渲染SVG元素
4145
- */
4146
- renderSvgElement(svgGroup, element) {
4147
- const { coordinates, style, metadata } = element;
4148
- if (coordinates.length === 0)
4149
- return;
4150
- const center = coordinates[0];
4151
- if (!metadata || !metadata.svg) {
4152
- this.renderSvgPlaceholder(svgGroup, center, metadata, style);
4153
- return;
4154
- }
4155
- try {
4156
- // 解析SVG字符串
4157
- const svgString = metadata.svg.replace(/\\n/g, '\n').replace(/\\"/g, '"');
4158
- const parser = new DOMParser();
4159
- const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');
4160
- const svgElement = svgDoc.documentElement;
4161
- if (svgElement.tagName === 'svg') {
4162
- // 获取原始SVG尺寸
4163
- const originalWidth = parseFloat(svgElement.getAttribute('width') || '139');
4164
- const originalHeight = parseFloat(svgElement.getAttribute('height') || '138');
4165
- // 计算变换参数
4166
- const userScale = metadata.scale || 1;
4167
- const direction = metadata.direction || 0;
4168
- // 设置原始SVG的实际尺寸(限制大小)
4169
- svgElement.setAttribute('width', originalWidth.toString());
4170
- svgElement.setAttribute('height', originalHeight.toString());
4171
- // 创建变换组
4172
- const transformGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
4173
- // 在transformGroup上应用变换:平移到中心,旋转,缩放,然后居中SVG
4174
- const transform = [
4175
- `translate(${center[0]}, ${center[1]})`,
4176
- `rotate(${-(direction * 180) / Math.PI})`,
4177
- `scale(${userScale})`,
4178
- `translate(${-originalWidth / 2}, ${-originalHeight / 2})`,
4179
- ].join(' ');
4180
- transformGroup.setAttribute('transform', transform);
4181
- // 设置样式
4182
- if (style.opacity !== undefined) {
4183
- transformGroup.setAttribute('opacity', style.opacity.toString());
4184
- }
4185
- // 将限制好尺寸的原始SVG添加到transformGroup中
4186
- transformGroup.appendChild(svgElement);
4187
- // 将transformGroup添加到svgGroup中
4188
- svgGroup.appendChild(transformGroup);
4189
- }
4190
- else {
4191
- this.renderSvgPlaceholder(svgGroup, center, metadata, style);
4192
- }
4193
- }
4194
- catch (error) {
4195
- console.warn('Failed to parse SVG:', error);
4196
- this.renderSvgPlaceholder(svgGroup, center, metadata, style);
4197
- }
4198
- }
4199
- /**
4200
- * 渲染SVG占位符
4201
- */
4202
- renderSvgPlaceholder(svgGroup, center, metadata, style) {
4203
- const size = (metadata?.scale || 1) * 20;
4204
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
4205
- rect.setAttribute('x', ((center[0] - size / 2) / 50).toString());
4206
- rect.setAttribute('y', ((center[1] - size / 2) / 50).toString());
4207
- rect.setAttribute('width', size.toString());
4208
- rect.setAttribute('height', size.toString());
4209
- rect.setAttribute('fill', style.fillColor);
4210
- rect.setAttribute('stroke', style.lineColor);
4211
- rect.setAttribute('stroke-width', style.lineWidth.toString());
4212
- rect.setAttribute('opacity', style.opacity.toString());
4213
- svgGroup.appendChild(rect);
4214
- }
4215
- }
4216
-
4217
- /**
4218
- * 边界图层
4219
- * 专门处理边界元素的渲染
4220
- */
4221
- class VisionOffLayer extends BaseLayer {
4222
- constructor() {
4223
- super();
4224
- this.type = LAYER_DEFAULT_TYPE.VISION_OFF_AREA;
4225
- this.level = 7; // 中等层级
4226
- }
4227
- /**
4228
- * SVG渲染方法
4229
- */
4230
- drawSVG(svgGroup) {
4231
- if (!this.visible || this.elements.length === 0) {
4232
- return;
4233
- }
4234
- // 只渲染边界类型的元素
4235
- for (const element of this.elements) {
4236
- if (element.type === 'vision_off_area') {
4237
- this.renderVisionOffArea(svgGroup, element);
4238
- }
4239
- }
4240
- }
4241
- /**
4242
- * 渲染边界元素
4243
- */
4244
- renderVisionOffArea(svgGroup, element) {
4245
- const { coordinates, style } = element;
4246
- if (coordinates.length < 3)
4247
- return;
4248
- const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
4249
- // 构建点集合,使用整数坐标
4250
- const points = coordinates.map((coord) => `${coord[0]},${coord[1]}`).join(' ');
4251
- const fillColor = style.fillColor || 'rgba(0, 255, 0, 0.4)';
4252
- polygon.setAttribute('points', points);
4253
- polygon.setAttribute('fill', fillColor);
4254
- polygon.setAttribute('stroke', style.lineColor);
4255
- // 确保最小线条宽度
4256
- const lineWidth = Math.max(style.lineWidth || 2, 0.5);
4257
- polygon.setAttribute('stroke-width', lineWidth.toString());
4258
- polygon.setAttribute('stroke-linecap', 'round');
4259
- polygon.setAttribute('stroke-linejoin', 'round');
4260
- polygon.setAttribute('opacity', (style.opacity || 1).toString());
4261
- polygon.setAttribute('vector-effect', 'non-scaling-stroke');
4262
- polygon.classList.add('vector-boundary');
4263
- svgGroup.appendChild(polygon);
4264
- }
4265
- }
4266
-
4267
3937
  /**
4268
3938
  * 边界相关样式配置
4269
3939
  */
@@ -7677,254 +7347,753 @@ var UnitsAreaType;
7677
7347
  })(UnitsAreaType || (UnitsAreaType = {}));
7678
7348
 
7679
7349
  /**
7680
- * 默认航向相对于canvas的偏移角度: 航向默认是东
7350
+ * 默认航向相对于canvas的偏移角度: 航向默认是东
7351
+ */
7352
+ const DIRECTION_DEGREE = 90; //正东相当于canvas的正北顺时针旋转90度
7353
+ /**
7354
+ * 矫正deltaTheta的范围,防止iot通道下因为车端theta接近π值时跳变造成app显示小车图标旋转的问题
7355
+ *
7356
+ * @param postureTheta
7357
+ * @return
7358
+ */
7359
+ function radNormalize(radian) {
7360
+ if (radian > Math.PI) {
7361
+ radian -= 2 * Math.PI; // 顺时针大于π时,减去2π
7362
+ }
7363
+ else if (radian < -Math.PI) {
7364
+ radian += 2 * Math.PI; // 逆时针小于-π时,加上2π
7365
+ }
7366
+ return radian;
7367
+ }
7368
+ // 将弧度转换为角度,并加上默认航向相对于canvas的偏移角度
7369
+ function radToDegree(radian) {
7370
+ return DIRECTION_DEGREE - radian * (180 / Math.PI);
7371
+ }
7372
+ // 计算两个点之间的间隔,单位是m
7373
+ function distance(x1, y1, x2, y2) {
7374
+ return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
7375
+ }
7376
+ const mathRound = (value, decimals = 2) => {
7377
+ return Number.isInteger(value) ? value : round(value, decimals);
7378
+ };
7379
+ const mathFloor = (value, decimals = 2) => {
7380
+ return Number.isInteger(value) ? value : floor(value, decimals);
7381
+ };
7382
+ // 公制平方米-》英制平方英尺
7383
+ function areaToft2(area) {
7384
+ return area * 10.7639104167;
7385
+ }
7386
+ //
7387
+ // 平方英尺-》英亩
7388
+ function ft2ToAcre(area) {
7389
+ return area / 43559.98;
7390
+ }
7391
+ /**
7392
+ * 将数字格式化为带有度量单位前缀的形式
7393
+ * @param value 需要格式化的数值
7394
+ * @param round 是否四舍五入,默认为true
7395
+ * @param decimals 保留小数位数,默认为2
7396
+ * @returns 格式化后的字符串,根据数值大小自动添加对应的度量单位前缀(k/M/B)
7397
+ * @example
7398
+ * formatNumberWithMetricPrefix(500) // 返回 "500"
7399
+ * formatNumberWithMetricPrefix(1500) // 返回 "1.50k"
7400
+ * formatNumberWithMetricPrefix(1500000) // 返回 "1.50M"
7401
+ * formatNumberWithMetricPrefix(2500000000) // 返回 "2.50B"
7402
+ */
7403
+ function formatNumberWithMetricPrefix(value, round = true, decimals = 2) {
7404
+ if (value === undefined)
7405
+ return value;
7406
+ if (value === 0)
7407
+ return '0';
7408
+ const mathFn = round ? mathRound : mathFloor;
7409
+ if (value < 1000) {
7410
+ return `${mathFn(value, decimals)}`;
7411
+ }
7412
+ else if (value < 1000000) {
7413
+ return `${mathFn(value / 1000, decimals)}K`;
7414
+ }
7415
+ else if (value < 1000000000) {
7416
+ return `${mathFn(value / 1000000, decimals)}M`;
7417
+ }
7418
+ else {
7419
+ return `${mathFn(value / 1000000000, decimals)}B`;
7420
+ }
7421
+ }
7422
+ /**
7423
+ * 转换割草面积的方法
7424
+ * @param area 面积数值(单位:m²)
7425
+ * @param type 单位类型 'metric' | 'imperial'
7426
+ * @returns {{ originNum: number; numStr?: string, value: string, unit: UnitsAreaType }} 返回格式化后的面积值和单位
7427
+ * @example
7428
+ * convertMowingArea(500, 'metric') // 返回 { value: "500", unit: "m²" }
7429
+ * convertMowingArea(1500, 'imperial') // 返回 { value: "16.1K", unit: "ft²" }
7430
+ * convertMowingArea(10000, 'imperial') // 返回 { value: "2.5", unit: "ac" }
7431
+ */
7432
+ function convertAreaByUnits(area, type) {
7433
+ if (area === undefined)
7434
+ return {
7435
+ originNum: 0,
7436
+ numStr: '0',
7437
+ value: `0 ${type === UnitsType.Metric
7438
+ ? UnitsAreaType.SQUARE_METER
7439
+ : UnitsAreaType.SQUARE_FOOT}`,
7440
+ unit: type === UnitsType.Metric
7441
+ ? UnitsAreaType.SQUARE_METER
7442
+ : UnitsAreaType.SQUARE_FOOT,
7443
+ };
7444
+ if (type === UnitsType.Metric) {
7445
+ return {
7446
+ originNum: area,
7447
+ numStr: formatNumberWithMetricPrefix(area, false),
7448
+ value: `${formatNumberWithMetricPrefix(area, false)} ${UnitsAreaType.SQUARE_METER}`,
7449
+ unit: UnitsAreaType.SQUARE_METER,
7450
+ };
7451
+ }
7452
+ // 将 m² 转换为 ft²(1 m² = 10.7639 ft²)
7453
+ const squareFeet = areaToft2(area);
7454
+ if (squareFeet < 10000) {
7455
+ return {
7456
+ originNum: squareFeet,
7457
+ numStr: formatNumberWithMetricPrefix(squareFeet, false),
7458
+ value: `${formatNumberWithMetricPrefix(squareFeet, false)} ${UnitsAreaType.SQUARE_FOOT}`,
7459
+ unit: UnitsAreaType.SQUARE_FOOT,
7460
+ };
7461
+ }
7462
+ const acres = ft2ToAcre(squareFeet);
7463
+ // 将 ft² 转换为 ac(1 ac = 43560 ft²)
7464
+ const result = formatNumberWithMetricPrefix(acres, false);
7465
+ return {
7466
+ originNum: acres,
7467
+ numStr: result,
7468
+ value: `${acres} ${UnitsAreaType.ACRE}`,
7469
+ unit: UnitsAreaType.ACRE,
7470
+ };
7471
+ }
7472
+
7473
+ /**
7474
+ * 日期时间格式化工具函数
7475
+ * 专门处理boundary中的日期时间显示格式
7476
+ */
7477
+ /**
7478
+ * 获取一周的开始日期(周日)
7479
+ * @param date 目标日期
7480
+ * @returns 该周的周日日期
7481
+ */
7482
+ function getWeekStart(date) {
7483
+ const startOfWeek = new Date(date);
7484
+ const dayOfWeek = startOfWeek.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
7485
+ // 向前推移到周日
7486
+ startOfWeek.setDate(startOfWeek.getDate() - dayOfWeek);
7487
+ startOfWeek.setHours(0, 0, 0, 0);
7488
+ return startOfWeek;
7489
+ }
7490
+ /**
7491
+ * 获取一周的结束日期(周六)
7492
+ * @param date 目标日期
7493
+ * @returns 该周的周六日期
7494
+ */
7495
+ function getWeekEnd(date) {
7496
+ const endOfWeek = new Date(date);
7497
+ const dayOfWeek = endOfWeek.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
7498
+ // 向后推移到周六
7499
+ endOfWeek.setDate(endOfWeek.getDate() + (6 - dayOfWeek));
7500
+ endOfWeek.setHours(23, 59, 59, 999);
7501
+ return endOfWeek;
7502
+ }
7503
+ /**
7504
+ * 判断两个日期是否为同一天
7505
+ * @param date1 第一个日期
7506
+ * @param date2 第二个日期
7507
+ * @returns 是否为同一天
7508
+ */
7509
+ function isSameDay(date1, date2) {
7510
+ return date1.getFullYear() === date2.getFullYear() &&
7511
+ date1.getMonth() === date2.getMonth() &&
7512
+ date1.getDate() === date2.getDate();
7513
+ }
7514
+ /**
7515
+ * 判断是否为今天
7516
+ * @param date 目标日期
7517
+ * @returns 是否为今天
7518
+ */
7519
+ function isToday(date) {
7520
+ const today = new Date();
7521
+ return isSameDay(date, today);
7522
+ }
7523
+ /**
7524
+ * 判断是否为昨天
7525
+ * @param date 目标日期
7526
+ * @returns 是否为昨天
7527
+ */
7528
+ function isYesterday(date) {
7529
+ const yesterday = new Date();
7530
+ yesterday.setDate(yesterday.getDate() - 1);
7531
+ return isSameDay(date, yesterday);
7532
+ }
7533
+ /**
7534
+ * 判断是否为本周内(以周日为一周的开始)
7535
+ * @param date 目标日期
7536
+ * @returns 是否为本周内
7537
+ */
7538
+ function isThisWeek(date) {
7539
+ const now = new Date();
7540
+ const weekStart = getWeekStart(now);
7541
+ const weekEnd = getWeekEnd(now);
7542
+ return date >= weekStart && date <= weekEnd;
7543
+ }
7544
+ /**
7545
+ * 格式化时间为 HH:mm 格式
7546
+ * @param date 目标日期
7547
+ * @returns 格式化后的时间字符串
7681
7548
  */
7682
- const DIRECTION_DEGREE = 90; //正东相当于canvas的正北顺时针旋转90度
7549
+ function formatTime(date) {
7550
+ const hours = String(date.getHours()).padStart(2, '0');
7551
+ const minutes = String(date.getMinutes()).padStart(2, '0');
7552
+ return `${hours}:${minutes}`;
7553
+ }
7683
7554
  /**
7684
- * 矫正deltaTheta的范围,防止iot通道下因为车端theta接近π值时跳变造成app显示小车图标旋转的问题
7555
+ * 获取星期几的英文缩写
7556
+ * @param date 目标日期
7557
+ * @returns 星期几的英文缩写
7558
+ */
7559
+ function getWeekdayAbbr(date) {
7560
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
7561
+ return weekdays[date.getDay()];
7562
+ }
7563
+ /**
7564
+ * 格式化boundary中的日期文本
7565
+ * 根据时间距离当前时间的远近,显示不同的格式:
7566
+ * - 今天:Today HH:mm
7567
+ * - 昨天:Yesterday HH:mm
7568
+ * - 本周内:Tue HH:mm
7569
+ * - 其他:MM/dd/yyyy HH:mm
7685
7570
  *
7686
- * @param postureTheta
7687
- * @return
7571
+ * @param timestamp 时间戳(秒)
7572
+ * @returns 格式化后的日期文本
7688
7573
  */
7689
- function radNormalize(radian) {
7690
- if (radian > Math.PI) {
7691
- radian -= 2 * Math.PI; // 顺时针大于π时,减去2π
7574
+ function formatBoundaryDateText(timestamp) {
7575
+ if (!timestamp || timestamp <= 0) {
7576
+ return '--/--/---- --:--';
7692
7577
  }
7693
- else if (radian < -Math.PI) {
7694
- radian += 2 * Math.PI; // 逆时针小于-π时,加上2π
7578
+ const date = new Date(timestamp * 1000); // 转换为毫秒
7579
+ const timeStr = formatTime(date);
7580
+ // 判断是否为今天
7581
+ if (isToday(date)) {
7582
+ return `Today ${timeStr}`;
7695
7583
  }
7696
- return radian;
7697
- }
7698
- // 将弧度转换为角度,并加上默认航向相对于canvas的偏移角度
7699
- function radToDegree(radian) {
7700
- return DIRECTION_DEGREE - radian * (180 / Math.PI);
7701
- }
7702
- // 计算两个点之间的间隔,单位是m
7703
- function distance(x1, y1, x2, y2) {
7704
- return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
7705
- }
7706
- const mathRound = (value, decimals = 2) => {
7707
- return Number.isInteger(value) ? value : round(value, decimals);
7708
- };
7709
- const mathFloor = (value, decimals = 2) => {
7710
- return Number.isInteger(value) ? value : floor(value, decimals);
7711
- };
7712
- // 公制平方米-》英制平方英尺
7713
- function areaToft2(area) {
7714
- return area * 10.7639104167;
7715
- }
7716
- //
7717
- // 平方英尺-》英亩
7718
- function ft2ToAcre(area) {
7719
- return area / 43559.98;
7584
+ // 判断是否为昨天
7585
+ if (isYesterday(date)) {
7586
+ return `Yesterday ${timeStr}`;
7587
+ }
7588
+ // 判断是否为本周内
7589
+ if (isThisWeek(date)) {
7590
+ const weekdayAbbr = getWeekdayAbbr(date);
7591
+ return `${weekdayAbbr} ${timeStr}`;
7592
+ }
7593
+ // 其他情况显示完整日期
7594
+ const month = String(date.getMonth() + 1).padStart(2, '0');
7595
+ const day = String(date.getDate()).padStart(2, '0');
7596
+ const year = date.getFullYear();
7597
+ return `${month}/${day}/${year} ${timeStr}`;
7720
7598
  }
7599
+
7721
7600
  /**
7722
- * 将数字格式化为带有度量单位前缀的形式
7723
- * @param value 需要格式化的数值
7724
- * @param round 是否四舍五入,默认为true
7725
- * @param decimals 保留小数位数,默认为2
7726
- * @returns 格式化后的字符串,根据数值大小自动添加对应的度量单位前缀(k/M/B)
7727
- * @example
7728
- * formatNumberWithMetricPrefix(500) // 返回 "500"
7729
- * formatNumberWithMetricPrefix(1500) // 返回 "1.50k"
7730
- * formatNumberWithMetricPrefix(1500000) // 返回 "1.50M"
7731
- * formatNumberWithMetricPrefix(2500000000) // 返回 "2.50B"
7601
+ * 障碍物图层
7602
+ * 专门处理障碍物元素的渲染
7732
7603
  */
7733
- function formatNumberWithMetricPrefix(value, round = true, decimals = 2) {
7734
- if (value === undefined)
7735
- return value;
7736
- if (value === 0)
7737
- return '0';
7738
- const mathFn = round ? mathRound : mathFloor;
7739
- if (value < 1000) {
7740
- return `${mathFn(value, decimals)}`;
7604
+ class ObstacleLayer extends BaseLayer {
7605
+ constructor() {
7606
+ super();
7607
+ this.level = 5;
7608
+ this.type = LAYER_DEFAULT_TYPE.OBSTACLE;
7741
7609
  }
7742
- else if (value < 1000000) {
7743
- return `${mathFn(value / 1000, decimals)}K`;
7610
+ /**
7611
+ * SVG渲染方法
7612
+ */
7613
+ drawSVG(svgGroup) {
7614
+ if (!this.visible || this.elements.length === 0) {
7615
+ return;
7616
+ }
7617
+ // 只渲染障碍物类型的元素
7618
+ for (const element of this.elements) {
7619
+ if (element.type === 'obstacle') {
7620
+ this.renderObstacle(svgGroup, element);
7621
+ }
7622
+ }
7744
7623
  }
7745
- else if (value < 1000000000) {
7746
- return `${mathFn(value / 1000000, decimals)}M`;
7624
+ /**
7625
+ * 将坐标点按type分组
7626
+ */
7627
+ groupCoordinatesByType(coordinates) {
7628
+ const segments = [];
7629
+ let currentSegment = null;
7630
+ for (let i = 0; i < coordinates.length; i++) {
7631
+ const coord = coordinates[i];
7632
+ const type = coord[2] || 2; // 默认type为2
7633
+ if (!currentSegment || currentSegment.type !== type) {
7634
+ // 开始新的段
7635
+ if (currentSegment && currentSegment.points.length > 0) {
7636
+ // 为了连接线段,将当前点也加入上一段的结尾
7637
+ currentSegment.points.push(coord);
7638
+ }
7639
+ currentSegment = {
7640
+ type: type,
7641
+ points: [coord],
7642
+ };
7643
+ segments.push(currentSegment);
7644
+ }
7645
+ else {
7646
+ // 继续当前段
7647
+ currentSegment.points.push(coord);
7648
+ }
7649
+ }
7650
+ // 处理封闭边界:如果第一段和最后一段type相同,需要连接起来
7651
+ if (segments.length > 1 && segments[0].type === segments[segments.length - 1].type) {
7652
+ const firstSegment = segments[0];
7653
+ const lastSegment = segments[segments.length - 1];
7654
+ // 将第一个点添加到最后一段,形成封闭
7655
+ lastSegment.points.push(firstSegment.points[0]);
7656
+ }
7657
+ else if (segments.length === 1) {
7658
+ // 只有一段的情况,添加第一个点到末尾形成封闭
7659
+ segments[0].points.push(coordinates[0]);
7660
+ }
7661
+ return segments;
7662
+ }
7663
+ /**
7664
+ * 渲染障碍物元素
7665
+ */
7666
+ renderObstacle(svgGroup, element) {
7667
+ const { coordinates, style, originalData } = element;
7668
+ const { status, start_timestamp, end_timestamp } = originalData || {};
7669
+ if (coordinates.length < 2 ||
7670
+ status === 0 ||
7671
+ start_timestamp > Date.now() / 1000 ||
7672
+ end_timestamp < Date.now() / 1000) {
7673
+ return;
7674
+ }
7675
+ // 1. 先遍历所有的coordinates,把所有点分为若干段的path
7676
+ const pathSegments = this.groupCoordinatesByType(coordinates);
7677
+ // 2&3. 根据type处理每个path段
7678
+ pathSegments.forEach((segment) => {
7679
+ if (segment.points.length < 2)
7680
+ return;
7681
+ if (segment.type === 2) {
7682
+ // type=2: 直接添加到svgGroup中
7683
+ this.createDirectPath(svgGroup, segment.points, style);
7684
+ }
7685
+ else if (segment.type === 1) {
7686
+ // type=1: 使用PathMeasure逻辑生成平行路径
7687
+ // this.createDirectPath(svgGroup, segment.points, style);
7688
+ this.createParallelPathsWithMeasure(svgGroup, segment.points, style);
7689
+ }
7690
+ });
7691
+ if (coordinates.length < 3)
7692
+ return;
7693
+ const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
7694
+ // 构建点集合,使用整数坐标
7695
+ const points = coordinates.map((coord) => `${coord[0]},${coord[1]}`).join(' ');
7696
+ polygon.setAttribute('points', points);
7697
+ polygon.setAttribute('fill', style.fillColor || 'rgba(220, 53, 69, 0.2)');
7698
+ polygon.setAttribute('stroke', 'transparent');
7699
+ // 确保最小线条宽度
7700
+ const lineWidth = Math.max(style.lineWidth || 2, 0.5);
7701
+ polygon.setAttribute('stroke-width', lineWidth.toString());
7702
+ polygon.setAttribute('stroke-linecap', 'round');
7703
+ polygon.setAttribute('stroke-linejoin', 'round');
7704
+ polygon.setAttribute('opacity', (style.opacity || 1).toString());
7705
+ polygon.setAttribute('vector-effect', 'non-scaling-stroke');
7706
+ polygon.classList.add('vector-obstacle');
7707
+ svgGroup.appendChild(polygon);
7708
+ }
7709
+ /**
7710
+ * 创建直接路径(type=2)
7711
+ */
7712
+ createDirectPath(svgGroup, points, style) {
7713
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
7714
+ const strokeColor = style.lineColor;
7715
+ // 构建路径数据
7716
+ let pathData = '';
7717
+ for (let i = 0; i < points.length; i++) {
7718
+ const [x, y] = points[i];
7719
+ if (i === 0) {
7720
+ pathData += `M ${x} ${y}`;
7721
+ }
7722
+ else {
7723
+ pathData += ` L ${x} ${y}`;
7724
+ }
7725
+ }
7726
+ path.setAttribute('d', pathData);
7727
+ path.setAttribute('stroke', strokeColor);
7728
+ path.setAttribute('fill', 'none');
7729
+ // 确保最小线条宽度
7730
+ const lineWidth = dp2px(style.lineWidth || 3);
7731
+ path.setAttribute('stroke-width', lineWidth.toString());
7732
+ path.setAttribute('stroke-linecap', 'round');
7733
+ path.setAttribute('stroke-linejoin', 'round');
7734
+ path.setAttribute('opacity', (style.opacity || 1).toString());
7735
+ path.setAttribute('vector-effect', 'non-scaling-stroke');
7736
+ path.classList.add('vector-obstacle');
7737
+ svgGroup.appendChild(path);
7738
+ }
7739
+ /**
7740
+ * 使用PathMeasure逻辑创建平行路径(type=1)
7741
+ */
7742
+ createParallelPathsWithMeasure(svgGroup, points, style) {
7743
+ const strokeColor = style.lineColor;
7744
+ const lineWidth = dp2px(style.lineWidth || 3);
7745
+ // 获取当前SVG的缩放级别,计算固定屏幕像素间距
7746
+ const fixedScreenDistance = lineWidth; // 固定的屏幕像素距离
7747
+ const offsetDistance = fixedScreenDistance; // 转换为SVG坐标系距离
7748
+ // 直接对每个线段生成平行直线段
7749
+ const parallelPaths = this.generateStraightParallelPaths(points, offsetDistance);
7750
+ // 渲染两条平行虚线
7751
+ parallelPaths.forEach((pathData, index) => {
7752
+ if (!pathData)
7753
+ return;
7754
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
7755
+ path.setAttribute('d', pathData);
7756
+ path.setAttribute('fill', 'none');
7757
+ path.setAttribute('stroke', strokeColor);
7758
+ path.setAttribute('stroke-width', lineWidth.toString());
7759
+ path.setAttribute('stroke-linecap', 'round');
7760
+ path.setAttribute('stroke-linejoin', 'round');
7761
+ path.setAttribute('opacity', (style.opacity || 1).toString());
7762
+ // 使用CSS样式设置虚线,避免随SVG缩放变化
7763
+ // 或者可以根据当前缩放级别动态计算dash array
7764
+ path.style.strokeDasharray = `${lineWidth}px ${lineWidth * 2}px`;
7765
+ path.classList.add(`vector-boundary-parallel-${index + 1}`);
7766
+ svgGroup.appendChild(path);
7767
+ });
7747
7768
  }
7748
- else {
7749
- return `${mathFn(value / 1000000000, decimals)}B`;
7769
+ /**
7770
+ * 生成直线平行路径(每个线段分别处理)
7771
+ */
7772
+ generateStraightParallelPaths(points, offsetDistance) {
7773
+ if (points.length < 2)
7774
+ return ['', ''];
7775
+ let parallelPath1Data = '';
7776
+ let parallelPath2Data = '';
7777
+ // 对每个线段分别计算平行线
7778
+ for (let i = 0; i < points.length - 1; i++) {
7779
+ const startPoint = points[i];
7780
+ const endPoint = points[i + 1];
7781
+ // 计算线段的方向向量
7782
+ const dx = endPoint[0] - startPoint[0];
7783
+ const dy = endPoint[1] - startPoint[1];
7784
+ const length = Math.sqrt(dx * dx + dy * dy);
7785
+ if (length === 0)
7786
+ continue; // 跳过零长度线段
7787
+ // 标准化方向向量
7788
+ const unitX = dx / length;
7789
+ const unitY = dy / length;
7790
+ // 计算垂直向量
7791
+ const perpendicularX = -unitY;
7792
+ const perpendicularY = unitX;
7793
+ // 计算平行线的起点和终点
7794
+ const start1X = startPoint[0] + perpendicularX * offsetDistance;
7795
+ const start1Y = startPoint[1] + perpendicularY * offsetDistance;
7796
+ const end1X = endPoint[0] + perpendicularX * offsetDistance;
7797
+ const end1Y = endPoint[1] + perpendicularY * offsetDistance;
7798
+ const start2X = startPoint[0] - perpendicularX * offsetDistance;
7799
+ const start2Y = startPoint[1] - perpendicularY * offsetDistance;
7800
+ const end2X = endPoint[0] - perpendicularX * offsetDistance;
7801
+ const end2Y = endPoint[1] - perpendicularY * offsetDistance;
7802
+ // 构建路径数据
7803
+ if (i === 0) {
7804
+ parallelPath1Data = `M ${start1X} ${start1Y}`;
7805
+ parallelPath2Data = `M ${start2X} ${start2Y}`;
7806
+ }
7807
+ else {
7808
+ parallelPath1Data += ` M ${start1X} ${start1Y}`;
7809
+ parallelPath2Data += ` M ${start2X} ${start2Y}`;
7810
+ }
7811
+ parallelPath1Data += ` L ${end1X} ${end1Y}`;
7812
+ parallelPath2Data += ` L ${end2X} ${end2Y}`;
7813
+ }
7814
+ return [parallelPath2Data, parallelPath1Data];
7750
7815
  }
7751
7816
  }
7817
+
7818
+ var chargingPileImage = "data:image/png;base64,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";
7819
+
7752
7820
  /**
7753
- * 转换割草面积的方法
7754
- * @param area 面积数值(单位:m²)
7755
- * @param type 单位类型 'metric' | 'imperial'
7756
- * @returns {{ originNum: number; numStr?: string, value: string, unit: UnitsAreaType }} 返回格式化后的面积值和单位
7757
- * @example
7758
- * convertMowingArea(500, 'metric') // 返回 { value: "500", unit: "m²" }
7759
- * convertMowingArea(1500, 'imperial') // 返回 { value: "16.1K", unit: "ft²" }
7760
- * convertMowingArea(10000, 'imperial') // 返回 { value: "2.5", unit: "ac" }
7821
+ * 充电桩图层
7822
+ * 专门处理充电桩元素的渲染
7761
7823
  */
7762
- function convertAreaByUnits(area, type) {
7763
- if (area === undefined)
7764
- return {
7765
- originNum: 0,
7766
- numStr: '0',
7767
- value: `0 ${type === UnitsType.Metric
7768
- ? UnitsAreaType.SQUARE_METER
7769
- : UnitsAreaType.SQUARE_FOOT}`,
7770
- unit: type === UnitsType.Metric
7771
- ? UnitsAreaType.SQUARE_METER
7772
- : UnitsAreaType.SQUARE_FOOT,
7773
- };
7774
- if (type === UnitsType.Metric) {
7775
- return {
7776
- originNum: area,
7777
- numStr: formatNumberWithMetricPrefix(area, false),
7778
- value: `${formatNumberWithMetricPrefix(area, false)} ${UnitsAreaType.SQUARE_METER}`,
7779
- unit: UnitsAreaType.SQUARE_METER,
7780
- };
7824
+ class ChargingPileLayer extends BaseLayer {
7825
+ constructor() {
7826
+ super();
7827
+ this.level = 8; // 中等层级
7828
+ this.type = LAYER_DEFAULT_TYPE.CHARGING_PILE;
7781
7829
  }
7782
- // 将 m² 转换为 ft²(1 m² = 10.7639 ft²)
7783
- const squareFeet = areaToft2(area);
7784
- if (squareFeet < 10000) {
7785
- return {
7786
- originNum: squareFeet,
7787
- numStr: formatNumberWithMetricPrefix(squareFeet, false),
7788
- value: `${formatNumberWithMetricPrefix(squareFeet, false)} ${UnitsAreaType.SQUARE_FOOT}`,
7789
- unit: UnitsAreaType.SQUARE_FOOT,
7790
- };
7830
+ /**
7831
+ * SVG渲染方法
7832
+ */
7833
+ drawSVG(svgGroup) {
7834
+ if (!this.visible || this.elements.length === 0) {
7835
+ return;
7836
+ }
7837
+ // 只渲染充电桩类型的元素
7838
+ for (const element of this.elements) {
7839
+ if (element.type === 'charging_pile') {
7840
+ this.renderChargingPile(svgGroup, element);
7841
+ }
7842
+ }
7843
+ }
7844
+ /**
7845
+ * 渲染充电桩元素
7846
+ */
7847
+ renderChargingPile(svgGroup, element) {
7848
+ const { coordinates, style } = element;
7849
+ if (coordinates.length === 0)
7850
+ return;
7851
+ const center = coordinates[0];
7852
+ const size = style.radius ? style.radius * 2 : 55; // 默认55px大小
7853
+ const direction = element.originalData?.direction || 0;
7854
+ // 将弧度转换为角度
7855
+ const angle = (direction * 180) / Math.PI;
7856
+ const rotationDegree = 270 - angle; // 坐标系转换
7857
+ const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
7858
+ // 图片居中定位
7859
+ const x = center[0];
7860
+ const y = center[1];
7861
+ image.setAttribute('x', x.toString());
7862
+ image.setAttribute('y', y.toString());
7863
+ image.setAttribute('width', `${size}px`);
7864
+ image.setAttribute('height', `${size}px`);
7865
+ image.setAttribute('href', chargingPileImage);
7866
+ image.setAttribute('opacity', '0'); // 初始透明
7867
+ // 添加SVG原生动画,传入默认角度
7868
+ this.addChargingPileAnimation(image, center, rotationDegree);
7869
+ image.classList.add('vector-charging-pile');
7870
+ svgGroup.appendChild(image);
7871
+ }
7872
+ /**
7873
+ * 添加充电桩SVG原生动画
7874
+ */
7875
+ addChargingPileAnimation(image, center, defaultAngle) {
7876
+ // 透明度动画 - 先显示出来
7877
+ const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
7878
+ animateOpacity.setAttribute('attributeName', 'opacity');
7879
+ animateOpacity.setAttribute('values', '0;1');
7880
+ animateOpacity.setAttribute('dur', '0.5s');
7881
+ animateOpacity.setAttribute('fill', 'freeze');
7882
+ image.appendChild(animateOpacity);
7883
+ // 旋转动画 - 从180度旋转到默认角度
7884
+ const animateTransform = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
7885
+ animateTransform.setAttribute('attributeName', 'transform');
7886
+ animateTransform.setAttribute('type', 'rotate');
7887
+ animateTransform.setAttribute('values', `${180 + defaultAngle} ${center[0]} ${center[1]};${defaultAngle} ${center[0]} ${center[1]}`);
7888
+ animateTransform.setAttribute('dur', '1s');
7889
+ animateTransform.setAttribute('repeatCount', '1'); // 只播放一次
7890
+ animateTransform.setAttribute('begin', '0.5s'); // 延迟0.5秒开始,等透明度动画完成
7891
+ animateTransform.setAttribute('fill', 'freeze'); // 保持最终状态
7892
+ image.appendChild(animateTransform);
7791
7893
  }
7792
- const acres = ft2ToAcre(squareFeet);
7793
- // 将 ft² 转换为 ac(1 ac = 43560 ft²)
7794
- const result = formatNumberWithMetricPrefix(acres, false);
7795
- return {
7796
- originNum: acres,
7797
- numStr: result,
7798
- value: `${acres} ${UnitsAreaType.ACRE}`,
7799
- unit: UnitsAreaType.ACRE,
7800
- };
7801
7894
  }
7802
7895
 
7803
7896
  /**
7804
- * 日期时间格式化工具函数
7805
- * 专门处理boundary中的日期时间显示格式
7806
- */
7807
- /**
7808
- * 获取一周的开始日期(周日)
7809
- * @param date 目标日期
7810
- * @returns 该周的周日日期
7811
- */
7812
- function getWeekStart(date) {
7813
- const startOfWeek = new Date(date);
7814
- const dayOfWeek = startOfWeek.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
7815
- // 向前推移到周日
7816
- startOfWeek.setDate(startOfWeek.getDate() - dayOfWeek);
7817
- startOfWeek.setHours(0, 0, 0, 0);
7818
- return startOfWeek;
7819
- }
7820
- /**
7821
- * 获取一周的结束日期(周六)
7822
- * @param date 目标日期
7823
- * @returns 该周的周六日期
7824
- */
7825
- function getWeekEnd(date) {
7826
- const endOfWeek = new Date(date);
7827
- const dayOfWeek = endOfWeek.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
7828
- // 向后推移到周六
7829
- endOfWeek.setDate(endOfWeek.getDate() + (6 - dayOfWeek));
7830
- endOfWeek.setHours(23, 59, 59, 999);
7831
- return endOfWeek;
7832
- }
7833
- /**
7834
- * 判断两个日期是否为同一天
7835
- * @param date1 第一个日期
7836
- * @param date2 第二个日期
7837
- * @returns 是否为同一天
7838
- */
7839
- function isSameDay(date1, date2) {
7840
- return date1.getFullYear() === date2.getFullYear() &&
7841
- date1.getMonth() === date2.getMonth() &&
7842
- date1.getDate() === date2.getDate();
7843
- }
7844
- /**
7845
- * 判断是否为今天
7846
- * @param date 目标日期
7847
- * @returns 是否为今天
7848
- */
7849
- function isToday(date) {
7850
- const today = new Date();
7851
- return isSameDay(date, today);
7852
- }
7853
- /**
7854
- * 判断是否为昨天
7855
- * @param date 目标日期
7856
- * @returns 是否为昨天
7857
- */
7858
- function isYesterday(date) {
7859
- const yesterday = new Date();
7860
- yesterday.setDate(yesterday.getDate() - 1);
7861
- return isSameDay(date, yesterday);
7862
- }
7863
- /**
7864
- * 判断是否为本周内(以周日为一周的开始)
7865
- * @param date 目标日期
7866
- * @returns 是否为本周内
7867
- */
7868
- function isThisWeek(date) {
7869
- const now = new Date();
7870
- const weekStart = getWeekStart(now);
7871
- const weekEnd = getWeekEnd(now);
7872
- return date >= weekStart && date <= weekEnd;
7873
- }
7874
- /**
7875
- * 格式化时间为 HH:mm 格式
7876
- * @param date 目标日期
7877
- * @returns 格式化后的时间字符串
7897
+ * 点图层
7898
+ * 专门处理点元素的渲染
7878
7899
  */
7879
- function formatTime(date) {
7880
- const hours = String(date.getHours()).padStart(2, '0');
7881
- const minutes = String(date.getMinutes()).padStart(2, '0');
7882
- return `${hours}:${minutes}`;
7900
+ class PointLayer extends BaseLayer {
7901
+ constructor() {
7902
+ super();
7903
+ this.level = 11;
7904
+ this.type = LAYER_DEFAULT_TYPE.POINT;
7905
+ }
7906
+ /**
7907
+ * SVG渲染方法
7908
+ */
7909
+ drawSVG(svgGroup) {
7910
+ if (!this.visible || this.elements.length === 0) {
7911
+ return;
7912
+ }
7913
+ // 只渲染点类型的元素
7914
+ for (const element of this.elements) {
7915
+ if (element.type === 'point') {
7916
+ this.renderPoint(svgGroup, element);
7917
+ }
7918
+ }
7919
+ }
7920
+ /**
7921
+ * 渲染点元素
7922
+ */
7923
+ renderPoint(svgGroup, element) {
7924
+ const { coordinates, style } = element;
7925
+ if (coordinates.length === 0)
7926
+ return;
7927
+ const coordinate = coordinates[0];
7928
+ const radius = style.radius || 4;
7929
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
7930
+ circle.setAttribute('cx', coordinate[0].toString());
7931
+ circle.setAttribute('cy', coordinate[1].toString());
7932
+ circle.setAttribute('r', radius.toString());
7933
+ circle.setAttribute('fill', style.fillColor || style.strokeColor || '#000000');
7934
+ circle.setAttribute('stroke', style.strokeColor || '#000000');
7935
+ // 确保最小线条宽度
7936
+ const lineWidth = Math.max(style.lineWidth || 1, 0.5);
7937
+ circle.setAttribute('stroke-width', lineWidth.toString());
7938
+ circle.setAttribute('opacity', (style.opacity || 1).toString());
7939
+ circle.setAttribute('vector-effect', 'non-scaling-stroke');
7940
+ circle.classList.add('vector-point');
7941
+ svgGroup.appendChild(circle);
7942
+ }
7883
7943
  }
7944
+
7884
7945
  /**
7885
- * 获取星期几的英文缩写
7886
- * @param date 目标日期
7887
- * @returns 星期几的英文缩写
7946
+ * SVG元素图层
7947
+ * 专门处理SVG元素的渲染
7888
7948
  */
7889
- function getWeekdayAbbr(date) {
7890
- const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
7891
- return weekdays[date.getDay()];
7949
+ class SvgElementLayer extends BaseLayer {
7950
+ constructor() {
7951
+ super();
7952
+ this.level = 6;
7953
+ this.scale = 1;
7954
+ this.type = LAYER_DEFAULT_TYPE.SVG;
7955
+ }
7956
+ /**
7957
+ * SVG渲染方法
7958
+ */
7959
+ drawSVG(svgGroup) {
7960
+ if (!this.visible || this.elements.length === 0) {
7961
+ return;
7962
+ }
7963
+ // 只渲染SVG类型的元素
7964
+ for (const element of this.elements) {
7965
+ const expirationTs = element.originalData?.expiration_ts;
7966
+ const current = Date.now() / 1000;
7967
+ if (expirationTs && current > expirationTs) {
7968
+ continue;
7969
+ }
7970
+ if (element.type === 'svg') {
7971
+ this.renderSvgElement(svgGroup, element);
7972
+ }
7973
+ }
7974
+ }
7975
+ /**
7976
+ * 渲染SVG元素
7977
+ */
7978
+ renderSvgElement(svgGroup, element) {
7979
+ const { coordinates, style, metadata } = element;
7980
+ if (coordinates.length === 0)
7981
+ return;
7982
+ const center = coordinates[0];
7983
+ if (!metadata || !metadata.svg) {
7984
+ this.renderSvgPlaceholder(svgGroup, center, metadata, style);
7985
+ return;
7986
+ }
7987
+ try {
7988
+ // 解析SVG字符串
7989
+ const svgString = metadata.svg.replace(/\\n/g, '\n').replace(/\\"/g, '"');
7990
+ const parser = new DOMParser();
7991
+ const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');
7992
+ const svgElement = svgDoc.documentElement;
7993
+ if (svgElement.tagName === 'svg') {
7994
+ // 获取原始SVG尺寸
7995
+ const originalWidth = parseFloat(svgElement.getAttribute('width') || '139');
7996
+ const originalHeight = parseFloat(svgElement.getAttribute('height') || '138');
7997
+ // 计算变换参数
7998
+ const userScale = metadata.scale || 1;
7999
+ const direction = metadata.direction || 0;
8000
+ // 设置原始SVG的实际尺寸(限制大小)
8001
+ svgElement.setAttribute('width', originalWidth.toString());
8002
+ svgElement.setAttribute('height', originalHeight.toString());
8003
+ // 创建变换组
8004
+ const transformGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
8005
+ // 在transformGroup上应用变换:平移到中心,旋转,缩放,然后居中SVG
8006
+ const transform = [
8007
+ `translate(${center[0]}, ${center[1]})`,
8008
+ `rotate(${-(direction * 180) / Math.PI})`,
8009
+ `scale(${userScale})`,
8010
+ `translate(${-originalWidth / 2}, ${-originalHeight / 2})`,
8011
+ ].join(' ');
8012
+ transformGroup.setAttribute('transform', transform);
8013
+ // 设置样式
8014
+ if (style.opacity !== undefined) {
8015
+ transformGroup.setAttribute('opacity', style.opacity.toString());
8016
+ }
8017
+ // 将限制好尺寸的原始SVG添加到transformGroup中
8018
+ transformGroup.appendChild(svgElement);
8019
+ // 将transformGroup添加到svgGroup中
8020
+ svgGroup.appendChild(transformGroup);
8021
+ }
8022
+ else {
8023
+ this.renderSvgPlaceholder(svgGroup, center, metadata, style);
8024
+ }
8025
+ }
8026
+ catch (error) {
8027
+ console.warn('Failed to parse SVG:', error);
8028
+ this.renderSvgPlaceholder(svgGroup, center, metadata, style);
8029
+ }
8030
+ }
8031
+ /**
8032
+ * 渲染SVG占位符
8033
+ */
8034
+ renderSvgPlaceholder(svgGroup, center, metadata, style) {
8035
+ const size = (metadata?.scale || 1) * 20;
8036
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
8037
+ rect.setAttribute('x', ((center[0] - size / 2) / 50).toString());
8038
+ rect.setAttribute('y', ((center[1] - size / 2) / 50).toString());
8039
+ rect.setAttribute('width', size.toString());
8040
+ rect.setAttribute('height', size.toString());
8041
+ rect.setAttribute('fill', style.fillColor);
8042
+ rect.setAttribute('stroke', style.lineColor);
8043
+ rect.setAttribute('stroke-width', style.lineWidth.toString());
8044
+ rect.setAttribute('opacity', style.opacity.toString());
8045
+ svgGroup.appendChild(rect);
8046
+ }
7892
8047
  }
8048
+
7893
8049
  /**
7894
- * 格式化boundary中的日期文本
7895
- * 根据时间距离当前时间的远近,显示不同的格式:
7896
- * - 今天:Today HH:mm
7897
- * - 昨天:Yesterday HH:mm
7898
- * - 本周内:Tue HH:mm
7899
- * - 其他:MM/dd/yyyy HH:mm
7900
- *
7901
- * @param timestamp 时间戳(秒)
7902
- * @returns 格式化后的日期文本
8050
+ * 边界图层
8051
+ * 专门处理边界元素的渲染
7903
8052
  */
7904
- function formatBoundaryDateText(timestamp) {
7905
- if (!timestamp || timestamp <= 0) {
7906
- return '--/--/---- --:--';
7907
- }
7908
- const date = new Date(timestamp * 1000); // 转换为毫秒
7909
- const timeStr = formatTime(date);
7910
- // 判断是否为今天
7911
- if (isToday(date)) {
7912
- return `Today ${timeStr}`;
8053
+ class VisionOffLayer extends BaseLayer {
8054
+ constructor() {
8055
+ super();
8056
+ this.type = LAYER_DEFAULT_TYPE.VISION_OFF_AREA;
8057
+ this.level = 7; // 中等层级
7913
8058
  }
7914
- // 判断是否为昨天
7915
- if (isYesterday(date)) {
7916
- return `Yesterday ${timeStr}`;
8059
+ /**
8060
+ * SVG渲染方法
8061
+ */
8062
+ drawSVG(svgGroup) {
8063
+ if (!this.visible || this.elements.length === 0) {
8064
+ return;
8065
+ }
8066
+ // 只渲染边界类型的元素
8067
+ for (const element of this.elements) {
8068
+ if (element.type === 'vision_off_area') {
8069
+ this.renderVisionOffArea(svgGroup, element);
8070
+ }
8071
+ }
7917
8072
  }
7918
- // 判断是否为本周内
7919
- if (isThisWeek(date)) {
7920
- const weekdayAbbr = getWeekdayAbbr(date);
7921
- return `${weekdayAbbr} ${timeStr}`;
8073
+ /**
8074
+ * 渲染边界元素
8075
+ */
8076
+ renderVisionOffArea(svgGroup, element) {
8077
+ const { coordinates, style } = element;
8078
+ if (coordinates.length < 3)
8079
+ return;
8080
+ const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
8081
+ // 构建点集合,使用整数坐标
8082
+ const points = coordinates.map((coord) => `${coord[0]},${coord[1]}`).join(' ');
8083
+ const fillColor = style.fillColor || 'rgba(0, 255, 0, 0.4)';
8084
+ polygon.setAttribute('points', points);
8085
+ polygon.setAttribute('fill', fillColor);
8086
+ polygon.setAttribute('stroke', style.lineColor);
8087
+ // 确保最小线条宽度
8088
+ const lineWidth = Math.max(style.lineWidth || 2, 0.5);
8089
+ polygon.setAttribute('stroke-width', lineWidth.toString());
8090
+ polygon.setAttribute('stroke-linecap', 'round');
8091
+ polygon.setAttribute('stroke-linejoin', 'round');
8092
+ polygon.setAttribute('opacity', (style.opacity || 1).toString());
8093
+ polygon.setAttribute('vector-effect', 'non-scaling-stroke');
8094
+ polygon.classList.add('vector-boundary');
8095
+ svgGroup.appendChild(polygon);
7922
8096
  }
7923
- // 其他情况显示完整日期
7924
- const month = String(date.getMonth() + 1).padStart(2, '0');
7925
- const day = String(date.getDate()).padStart(2, '0');
7926
- const year = date.getFullYear();
7927
- return `${month}/${day}/${year} ${timeStr}`;
7928
8097
  }
7929
8098
 
7930
8099
  /**
@@ -9174,7 +9343,12 @@ class BoundaryLabelsManager {
9174
9343
  labelDiv.style.whiteSpace = 'nowrap';
9175
9344
  labelDiv.style.maxWidth = '220px';
9176
9345
  labelDiv.style.transform = `translate(-50%, -50%) rotate(${-this.rotation}deg)`;
9177
- labelDiv.style.pointerEvents = 'auto';
9346
+ if (this.onlyRead) {
9347
+ labelDiv.style.pointerEvents = 'none';
9348
+ }
9349
+ else {
9350
+ labelDiv.style.pointerEvents = 'auto';
9351
+ }
9178
9352
  labelDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.4)';
9179
9353
  labelDiv.style.cursor = 'pointer';
9180
9354
  labelDiv.style.transition = 'background-color 0.2s ease';
@@ -9260,6 +9434,9 @@ class BoundaryLabelsManager {
9260
9434
  * 展开标签
9261
9435
  */
9262
9436
  expandLabel(boundaryId) {
9437
+ if (this.onlyRead) {
9438
+ return;
9439
+ }
9263
9440
  const labelDiv = this.getLabelElement(boundaryId);
9264
9441
  if (!labelDiv)
9265
9442
  return;
@@ -9268,12 +9445,6 @@ class BoundaryLabelsManager {
9268
9445
  return;
9269
9446
  // 关闭其他展开的标签
9270
9447
  this.collapseOtherLabels(boundaryId);
9271
- if (this.onlyRead) {
9272
- extendedContent.style.pointerEvents = 'none';
9273
- return;
9274
- }
9275
- // 展开当前标签
9276
- extendedContent.style.pointerEvents = 'auto';
9277
9448
  extendedContent.style.display = 'block';
9278
9449
  this.currentExpandedBoundaryId = boundaryId;
9279
9450
  }
@@ -9473,6 +9644,16 @@ class BoundaryLabelsManager {
9473
9644
  const pixelY = relativeY * divHeight;
9474
9645
  return { x: pixelX, y: pixelY };
9475
9646
  }
9647
+ updateReadOnlyMode(onlyRead) {
9648
+ this.onlyRead = onlyRead;
9649
+ if (!this.container)
9650
+ return;
9651
+ const allLabels = this.container.querySelectorAll('.boundary-label');
9652
+ allLabels.forEach((label) => {
9653
+ const labelElement = label;
9654
+ labelElement.style.pointerEvents = onlyRead ? 'none' : 'auto';
9655
+ });
9656
+ }
9476
9657
  /**
9477
9658
  * 更新边界数据
9478
9659
  */
@@ -9867,7 +10048,12 @@ class AntennaManager {
9867
10048
  antennaContainer.className = 'antenna-container-item';
9868
10049
  antennaContainer.style.position = 'absolute';
9869
10050
  antennaContainer.style.transform = `translate(-50%, -50%) rotate(${-this.rotation}deg)`;
9870
- antennaContainer.style.pointerEvents = 'auto';
10051
+ if (this.onlyRead) {
10052
+ antennaContainer.style.pointerEvents = 'none';
10053
+ }
10054
+ else {
10055
+ antennaContainer.style.pointerEvents = 'auto';
10056
+ }
9871
10057
  antennaContainer.style.zIndex = AntennaManager.Z_INDEX.DEFAULT.toString();
9872
10058
  antennaContainer.setAttribute('data-antenna-id', antennaData.type.toString());
9873
10059
  // 创建天线图标
@@ -9895,9 +10081,6 @@ class AntennaManager {
9895
10081
  // 添加点击事件
9896
10082
  antennaDiv.addEventListener('click', (e) => {
9897
10083
  e.stopPropagation();
9898
- if (this.onlyRead) {
9899
- return;
9900
- }
9901
10084
  // 关闭其他展开的tooltip
9902
10085
  this.collapseOtherTooltips(antennaContainer);
9903
10086
  this.elevateAntennaZIndex();
@@ -9913,15 +10096,9 @@ class AntennaManager {
9913
10096
  });
9914
10097
  // 添加悬停效果
9915
10098
  antennaDiv.addEventListener('mouseenter', () => {
9916
- if (this.onlyRead) {
9917
- return;
9918
- }
9919
10099
  antennaDiv.style.transform = 'scale(1.1)';
9920
10100
  });
9921
10101
  antennaDiv.addEventListener('mouseleave', () => {
9922
- if (this.onlyRead) {
9923
- return;
9924
- }
9925
10102
  antennaDiv.style.transform = 'scale(1)';
9926
10103
  });
9927
10104
  antennaContainer.appendChild(antennaDiv);
@@ -9974,10 +10151,8 @@ class AntennaManager {
9974
10151
  if (!tooltip)
9975
10152
  return;
9976
10153
  if (this.onlyRead) {
9977
- tooltip.style.pointerEvents = 'none';
9978
10154
  return;
9979
10155
  }
9980
- tooltip.style.pointerEvents = 'auto';
9981
10156
  // 展开当前tooltip
9982
10157
  tooltip.style.display = 'block';
9983
10158
  const extendedContent = tooltip.querySelector('.antenna-tooltip-extended');
@@ -10023,6 +10198,16 @@ class AntennaManager {
10023
10198
  }
10024
10199
  });
10025
10200
  }
10201
+ updateReadOnlyMode(onlyRead) {
10202
+ this.onlyRead = onlyRead;
10203
+ if (!this.container)
10204
+ return;
10205
+ const allAntennas = this.container.querySelectorAll('.antenna-container-item');
10206
+ allAntennas.forEach((antenna) => {
10207
+ const antennaElement = antenna;
10208
+ antennaElement.style.pointerEvents = onlyRead ? 'none' : 'auto';
10209
+ });
10210
+ }
10026
10211
  /**
10027
10212
  * 添加主天线
10028
10213
  */
@@ -10763,6 +10948,7 @@ class MowerMapOverlay {
10763
10948
  this.overlayView.hide = this.hide.bind(this);
10764
10949
  // 添加编辑模式相关方法
10765
10950
  this.overlayView.setEditMode = this.setEditMode.bind(this);
10951
+ this.overlayView.setReadOnlyMode = this.setReadOnlyMode.bind(this);
10766
10952
  this.overlayView.getEditData = this.getEditData.bind(this);
10767
10953
  this.overlayView.handleSave = this.handleSave.bind(this);
10768
10954
  this.overlayView.setCustomIcons = this.setCustomIcons.bind(this);
@@ -10984,6 +11170,15 @@ class MowerMapOverlay {
10984
11170
  this.chargingPileManager.updatePositions();
10985
11171
  }
10986
11172
  }
11173
+ // 设置编辑模式
11174
+ setReadOnlyMode(enabled) {
11175
+ if (this.boundaryLabelsManager) {
11176
+ this.boundaryLabelsManager.updateReadOnlyMode(enabled);
11177
+ }
11178
+ if (this.antennaManager) {
11179
+ this.antennaManager.updateReadOnlyMode(enabled);
11180
+ }
11181
+ }
10987
11182
  // 创建编辑界面
10988
11183
  createEditInterface() {
10989
11184
  if (!this.div)
@@ -12170,6 +12365,12 @@ sn, edger = false, unitType = UnitsType.Imperial, language = 'en', onlyRead = fa
12170
12365
  overlayRef.current.setEditMode(isEditMode);
12171
12366
  }
12172
12367
  }, [isEditMode]);
12368
+ // 监听只读模式变化
12369
+ React.useEffect(() => {
12370
+ if (overlayRef.current) {
12371
+ overlayRef.current.setReadOnlyMode(onlyRead);
12372
+ }
12373
+ }, [onlyRead]);
12173
12374
  // 监听路径信息的更新,需要同步更新boundaary的进度信息
12174
12375
  // useEffect(() => {
12175
12376
  // if (!mapJson) return;