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