@@ -100,20 +99,21 @@
right: 0;
top: 0;
bottom: 0;
- //z-index: 1;
+ z-index: 2;
color: var(--td-text-color-primary);
width: var(--width);
background-color: var(--td-bg-color-page);
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
box-sizing: border-box;
transition: all 0.2s ease;
+ transform: translateX(100%);
- &:global.expand{
- transform: translateX(100%);
- }
+ //&:global.expand{
+ // transform: translateX(100%);
+ //}
&:global.is-open {
- transform: translateX(0px);
+ transform: translateX(0%);
}
:global {
@@ -141,16 +141,17 @@
border-radius: var(--td-radius-medium);
box-sizing: border-box;
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
- box-shadow: 0 0 1px 0 var(--td-component-border);
+ //box-shadow: 0 0 1px 0 var(--td-component-border);
+ border: 1px solid var(--td-component-stroke);
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
.interspace {
flex: 1;
}
- .t-space .t-icon {
- font-size: var(--td-font-size-headline-medium);
- }
+ //.t-space .t-icon {
+ // font-size: var(--td-font-size-headline-medium);
+ //}
}
.top-bar {
@@ -162,7 +163,8 @@
//padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
overflow: auto;
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
- box-shadow: 0 0 1px 0 var(--td-component-border);
+ //box-shadow: 0 0 1px 0 var(--td-component-border);
+ border: 1px solid var(--td-component-stroke);
background-color: var(--td-bg-color-container);
border-radius: var(--td-radius-medium);
}
diff --git a/src/components/ImageUploader.vue b/src/components/ImageUploader.vue
index 3f870e9..4c6b009 100644
--- a/src/components/ImageUploader.vue
+++ b/src/components/ImageUploader.vue
@@ -87,6 +87,15 @@
.t-upload__card-name {
display: none
}
+
+ .t-upload__card-image {
+ width: 100%;
+ height: 100%;
+
+ .t-image {
+ object-fit: cover;
+ }
+ }
}
}
diff --git a/src/components/ManufacturerSelector.vue b/src/components/ManufacturerSelector.vue
new file mode 100644
index 0000000..51b789d
--- /dev/null
+++ b/src/components/ManufacturerSelector.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/src/components/MultipleFilesUploader.vue b/src/components/MultipleFilesUploader.vue
new file mode 100644
index 0000000..2b01679
--- /dev/null
+++ b/src/components/MultipleFilesUploader.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/src/components/TextRow.vue b/src/components/TextRow.vue
new file mode 100644
index 0000000..879f8c1
--- /dev/null
+++ b/src/components/TextRow.vue
@@ -0,0 +1,31 @@
+
+
+
+
+ - {{ label }}
+
+
+
+
+
diff --git a/src/components/TrackPlaybackController.vue b/src/components/TrackPlaybackController.vue
new file mode 100644
index 0000000..a0dab60
--- /dev/null
+++ b/src/components/TrackPlaybackController.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+ {{ speedRate }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TrackTimelyInfo.vue b/src/components/TrackTimelyInfo.vue
new file mode 100644
index 0000000..30ffdf7
--- /dev/null
+++ b/src/components/TrackTimelyInfo.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+ {{ formatTime(info.timestamp) }}
+ {{ toFixed(info.lng, 7) }}
+ {{ toFixed(info.lat, 7) }}
+ {{ toFixed(info.spd / 10, 2) }}
+
+
+ {{ formatTime(info.timestamp) }}
+ {{ toFixed(info.lng, 7) }}
+ {{ toFixed(info.lat, 7) }}
+ {{ toFixed(info.height, 2) }}
+ {{ toFixed(info.xspeed, 2) }}
+ {{ toFixed(info.yspeed, 2) }}
+ {{ toFixed(info.yaw, 2) }}
+ {{ toFixed(info.ra, 2) }}
+ {{ toFixed(info.pa, 2) }}
+ {{ toFixed(info.flowSpeed, 2) }}
+ {{ toFixed(info.dose, 2) }}
+
+
+
+
+
diff --git a/src/components/VideoPlayer.vue b/src/components/VideoPlayer.vue
new file mode 100644
index 0000000..ea7151e
--- /dev/null
+++ b/src/components/VideoPlayer.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+ 视频列表
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 还没有上传视频哦 ~
+
+
+
+
+
+
+
+
diff --git a/src/config/urls.js b/src/config/urls.js
index 5a3e97a..371f723 100644
--- a/src/config/urls.js
+++ b/src/config/urls.js
@@ -13,6 +13,9 @@ export const REQUEST_UPLOAD_FILE = (subject) => buildURL(`${BASE_URL}/fileServ/v
// 登录
export const LOGIN_WITH_PASSWORD = `${BASE_URL}/userServ/v1/login`;
+// 更改密码
+export const UPDATE_PASSWORD = `${BASE_URL}/userServ/v1/users/{id}`;
+
// 禁飞区管理
export const GET_NO_FLY_ZONE_LIST = `${BASE_URL}/mainServ/v1/noFlyZones`;
export const GET_NO_FLY_ZONE_DETAIL = `${BASE_URL}/mainServ/v1/noFlyZones/{id}`;
@@ -28,9 +31,42 @@ export const UPDATE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
export const DELETE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
-// 制造商管理
+// 监管者管理
export const GET_REGULATOR_LIST = `${BASE_URL}/userServ/v1/regulators`;
export const CREATE_REGULATOR = `${BASE_URL}/userServ/v1/regulator`;
// export const UPDATE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
// export const DELETE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
// export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
+
+// 设备管理
+export const GET_DEVICE_LIST = `${BASE_URL}/mainServ/v1/devices`;
+export const GET_DEVICE_DETAIL = `${BASE_URL}/mainServ/v1/devices/{id}`;
+export const CREATE_DEVICE = `${BASE_URL}/mainServ/v1/devices`;
+export const UPDATE_DEVICE = `${BASE_URL}/mainServ/v1/devices/{id}`;
+export const DELETE_DEVICE = `${BASE_URL}/mainServ/v1/devices/{id}`;
+export const UPDATE_DEVICE_LOCKED = `${BASE_URL}/mainServ/v1/devices/{id}/locked`;
+
+// 实时监控
+export const GET_ONLINE_DEVICE = `${BASE_URL}/flightServ/v1/dashboard/onlineDrones`;
+
+// 架次管理
+export const GET_SORTIE_LIST = `${BASE_URL}/flightServ/v1/sorties`;
+export const GET_SORTIE_DETAIL = `${BASE_URL}/flightServ/v1/sorties/{id}`;
+// export const CREATE_SORTIE = `${BASE_URL}/mainServ/v1/devices`;
+// export const UPDATE_SORTIE = `${BASE_URL}/mainServ/v1/devices/{id}`;
+// export const DELETE_SORTIE = `${BASE_URL}/mainServ/v1/devices/{id}`;
+// export const UPDATE_SORTIE_LOCKED = `${BASE_URL}/mainServ/v1/devices/{id}/locked`;
+
+// 媒体
+export const GET_MEDIA_LIST = `${BASE_URL}/flightServ/v1/sorties/{id}/medias`;
+export const CREATE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/{id}/medias`;
+export const DELETE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/medias/{id}`;
+
+// 成果管理
+export const GET_ACHIEVEMENT_LIST = `${BASE_URL}/flightServ/v1/sorties/outcomes`;
+export const CREATE_ACHIEVEMENT_SHARE_CODE = `${BASE_URL}/flightServ/v1/sorties/shareCode`;
+export const BINDING_ACHIEVEMENT = `${BASE_URL}/flightServ/v1/sorties/outcomes/{id}`;
+// export const DELETE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/medias/{id}`;
+
+// 直播
+export const GET_PULL_STREAM_URL = `${BASE_URL}/flightServ/v1/sorties/pullStreamUrl/{id}`;
diff --git a/src/core/Enclosure.js b/src/core/Enclosure.js
new file mode 100644
index 0000000..d2e5457
--- /dev/null
+++ b/src/core/Enclosure.js
@@ -0,0 +1,264 @@
+/**
+ * 圈地(算面积)
+ */
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+
+const SOURCE_LINE = 'enclosure-source-line';
+const SOURCE_FILL = 'enclosure-source-fill';
+const SOURCE_SYMBOL = 'enclosure-source-symbol';
+const LAYER_LINE = 'enclosure-layer-line';
+const LAYER_FILL = 'enclosure-layer-fill';
+const LAYER_SYMBOL = 'enclosure-layer-symbol';
+
+class Enclosure {
+ // 地图实例
+ map = null;
+
+ // 点坐标
+ coordinates = [];
+
+ // 操作点
+ markers = [];
+
+ // 形状是否已经闭合(即:是否已经圈完地了)
+ isClosed = false;
+
+ // 形状是否扭结了
+ isKinked = false;
+
+ // 视觉选项
+ option = {
+ edgeColor: 'rgba(0, 0, 255, 0.8)',
+ fillColor: 'rgba(0, 0, 255, 0.3)',
+ markerColor: 'rgba(255, 255, 255, 0.8)',
+ labelColor: 'rgba(255, 255, 255, 0.8)',
+ labelStrokeColor: 'rgba(0, 0, 0, 1)',
+ labelFontSize: 14,
+ };
+
+ constructor(map, option = {}) {
+ this.map = map;
+ this.option = {
+ ...this.option,
+ ...option,
+ };
+ }
+
+ onDrawOn() {
+ this.map.getCanvas().style.cursor = 'crosshair';
+ this.coordinates = [];
+ this.markers = [];
+ this.isClosed = false;
+ this.map.on('click', this.onClickMap);
+ this.map.on('style.load', this.draw);
+ this.map.fire('enclosure.on');
+ }
+
+ onDrawOff() {
+ this.map.getCanvas().style.cursor = '';
+ if (this.map.getSource(SOURCE_LINE)) {
+ this.map.removeLayer(LAYER_LINE);
+ this.map.removeSource(SOURCE_LINE);
+ }
+ if (this.map.getSource(SOURCE_FILL)) {
+ this.map.removeLayer(LAYER_FILL);
+ this.map.removeSource(SOURCE_FILL);
+ }
+ if (this.map.getSource(SOURCE_SYMBOL)) {
+ this.map.removeLayer(LAYER_SYMBOL);
+ this.map.removeSource(SOURCE_SYMBOL);
+ }
+ this.markers.forEach(m => m.remove());
+ this.map.off('click', this.onClickMap);
+ this.map.off('style.load', this.draw);
+ this.map.fire('enclosure.off');
+ }
+
+ // 生成线段geojson
+ genLineFeature() {
+ const [first] = this.coordinates;
+ const coordinates = this.isClosed ? [...this.coordinates, first] : this.coordinates;
+ return turf.lineString(coordinates);
+ }
+
+ // 生成多边形geojson
+ // eslint-disable-next-line class-methods-use-this
+ genPolygonFeature(lineFeature) {
+ return turf.lineToPolygon(lineFeature);
+ }
+
+ // 检测形状是否扭结(判断新坐标加入后,或者对原有坐标进行判断)
+ checkKinked(newCoordinate) {
+ if (this.coordinates.length < 3) return false;
+ if (newCoordinate) {
+ const lineFeature = turf.lineString([...this.coordinates, newCoordinate]);
+ const kinks = turf.kinks(lineFeature);
+ return !!kinks.features.length;
+ }
+ const lineFeature = turf.lineString(this.coordinates);
+ const polygonFeature = turf.lineToPolygon(lineFeature);
+ const kinks = turf.kinks(polygonFeature);
+ return !!kinks.features.length;
+ }
+
+ draw = () => {
+ const { length: count } = this.coordinates;
+ if (count < 2) return;
+
+ // 显示边线
+ const sourceLine = this.map.getSource(SOURCE_LINE);
+ const lineFeature = this.genLineFeature();
+ if (!sourceLine) {
+ this.map.addSource(SOURCE_LINE, {
+ type: 'geojson',
+ data: lineFeature,
+ });
+
+ this.map.addLayer({
+ id: LAYER_LINE,
+ type: 'line',
+ source: SOURCE_LINE,
+ paint: {
+ 'line-color': this.option.edgeColor,
+ 'line-width': 2,
+ },
+ });
+ } else {
+ sourceLine.setData(lineFeature);
+ }
+
+ if (count < 3) return;
+
+ // 显示多边形
+ const sourceFill = this.map.getSource(SOURCE_FILL);
+ const polygonFeature = this.genPolygonFeature(lineFeature);
+ if (!sourceFill) {
+ this.map.addSource(SOURCE_FILL, {
+ type: 'geojson',
+ data: polygonFeature,
+ });
+
+ this.map.addLayer({
+ id: LAYER_FILL,
+ type: 'fill',
+ source: SOURCE_FILL,
+ paint: {
+ 'fill-color': this.option.fillColor,
+ },
+ });
+ } else {
+ sourceFill.setData(polygonFeature);
+ }
+
+ if (!this.isClosed) return;
+
+ const area = this.isKinked ? '0' : (turf.area(polygonFeature) * 0.0015).toFixed(2); // 亩
+ const pointFeature = turf.centerOfMass(polygonFeature);
+ pointFeature.properties.area = this.isKinked ? '' : `${area}亩`;
+
+ const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
+ if (!sourceSymbol) {
+ this.map.addSource(SOURCE_SYMBOL, {
+ type: 'geojson',
+ data: pointFeature,
+ });
+
+ this.map.addLayer({
+ id: LAYER_SYMBOL,
+ type: 'symbol',
+ source: SOURCE_SYMBOL,
+ layout: {
+ 'text-field': '{area}',
+ 'text-anchor': 'top',
+ 'text-size': this.option.labelFontSize,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': this.option.labelColor,
+ 'text-halo-color': this.option.labelStrokeColor,
+ 'text-halo-width': 1,
+ },
+ });
+ } else {
+ sourceSymbol.setData(pointFeature);
+ }
+ };
+
+ // 闭合路径
+ closePath() {
+ this.isClosed = true;
+ if (this.checkKinked()) {
+ this.isKinked = true;
+ this.map.fire('enclosure.kinked', { action: 'drag' });
+ } else {
+ this.isKinked = false;
+ }
+ this.draw();
+ }
+
+ // 生成操作点
+ genMarkerNode() {
+ const node = document.createElement('div');
+ node.style.cursor = 'pointer';
+ node.style.width = '12px';
+ node.style.height = '12px';
+ node.style.borderRadius = '50%';
+ node.style.background = this.option.markerColor;
+ node.style.boxSizing = 'border-box';
+ node.style.border = `2px solid ${this.option.edgeColor}`;
+ node.addEventListener('click', e => {
+ e.stopPropagation();
+ const [first] = this.markers;
+ // 当有3个以上的点出现,就可以点击第一个点,闭合多边形
+ if (first && (first.getElement() === node) && this.coordinates.length >= 3) {
+ this.closePath();
+ }
+ }, false);
+ return node;
+ }
+
+ onClickMap = e => {
+ if (this.isClosed) return;
+
+ const { lng, lat } = e.lngLat;
+ // 判断新坐标是否会造成扭结(即:新的边线是否会跟其他边线交叉)
+ if (this.checkKinked([lng, lat])) {
+ this.map.fire('enclosure.kinked', { action: 'click' });
+ return;
+ }
+
+ const marker = new mapbox.Marker({
+ element: this.genMarkerNode(),
+ draggable: true,
+ }).setLngLat(e.lngLat).addTo(this.map);
+
+ this.coordinates.push([lng, lat]);
+ this.draw();
+ this.markers.push(marker);
+ this.map.fire('enclosure.change', { coordinates: this.coordinates });
+
+ marker.on('drag', () => {
+ const index = this.markers.indexOf(marker);
+ const { lng: newLng, lat: newLat } = marker.getLngLat();
+ this.coordinates[index] = [newLng, newLat];
+ if (this.checkKinked()) {
+ this.isKinked = true;
+ this.map.fire('enclosure.kinked', { action: 'drag' });
+ } else {
+ // 从扭结复原了
+ if (this.isKinked) {
+ this.map.fire('enclosure.kink-recovered');
+ }
+ this.isKinked = false;
+ }
+ this.draw();
+ });
+
+ marker.on('dragend', () => {
+ this.map.fire('enclosure.change', { coordinates: this.coordinates });
+ });
+ };
+}
+
+export default Enclosure;
diff --git a/src/core/IconPoint/Icon.js b/src/core/IconPoint/Icon.js
new file mode 100644
index 0000000..7be1853
--- /dev/null
+++ b/src/core/IconPoint/Icon.js
@@ -0,0 +1,34 @@
+/**
+ * 图标
+ */
+
+const check = Symbol('Icon.check');
+
+class Icon {
+ constructor(name, image, zoom, offset) {
+ Icon[check](image);
+
+ this.name = name;
+ this.zoom = zoom || 1;
+ this.offset = offset || [0, 0];
+ this.data = image;
+ }
+
+ update(image) {
+ Icon[check](image);
+ this.data = image;
+ }
+
+ static [check](image) {
+ if (
+ !(image instanceof HTMLImageElement)
+ && !(image instanceof ImageBitmap)
+ && !(image instanceof ImageData)
+ && !('data' in image && 'width' in image && 'height' in image)
+ ) {
+ throw new Error('image类型错误');
+ }
+ }
+}
+
+export default Icon;
diff --git a/src/core/IconPoint/IconPointRenderer.js b/src/core/IconPoint/IconPointRenderer.js
new file mode 100644
index 0000000..2956e3f
--- /dev/null
+++ b/src/core/IconPoint/IconPointRenderer.js
@@ -0,0 +1,234 @@
+/**
+ * 带图标的点
+ */
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import EventDispatcher from '../EventDispatcher';
+import Icon from './Icon';
+
+class IconPointRenderer extends EventDispatcher {
+ _topic = null;
+
+ _map = null;
+
+ _icons = {};
+
+ _dataSource = [];
+
+ get _sourceId() {
+ return `${this._topic}-icon-point-source`;
+ }
+
+ get _layerId() {
+ return `${this._topic}-icon-point-layer`;
+ }
+
+ get _pointFeatures() {
+ return this._dataSource.map(({ id, iconName, point, ...others }) => {
+ const { [iconName]: icon } = this._icons;
+ const { zoom: iconZoom = 1, offset: iconOffset = [0, 0] } = icon || {};
+ const feature = turf.point(point, { id, iconName, iconZoom, iconOffset, ...others });
+ feature.id = id;
+ return feature;
+ });
+ }
+
+ get _pointFeatureCollection() {
+ return turf.featureCollection(this._pointFeatures);
+ }
+
+ constructor(topic, icons = []) {
+ super(['mousemove', 'mouseleave', 'mouseenter']);
+ this._topic = topic;
+ if (!icons.length) {
+ throw new Error('入参icons至少包含一个图标元素');
+ }
+ icons.forEach(this.updateIcon);
+
+ makeObservable(this, {
+ _topic: observable,
+ _icons: observable,
+ _dataSource: observable,
+ _sourceId: computed,
+ _layerId: computed,
+ _pointFeatures: computed,
+ _pointFeatureCollection: computed,
+ loadDataSource: action,
+ updateIcon: action,
+ destroy: action,
+ });
+
+ reaction(() => this._dataSource, () => {
+ this._render();
+ });
+ reaction(() => this._icons, () => {
+ this._render();
+ });
+ }
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ // 载入数据
+ // list->item必须包含id、point、iconName(其中point如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
+ // list->item可以包含rotate,表示旋转角度
+ loadDataSource(list) {
+ if (!this._map) {
+ throw new Error('请先设置地图实例');
+ }
+ this._dataSource = (list || []).map(item => {
+ const { id, iconName, point } = item;
+ const newItem = { ...item };
+ if (!Array.isArray(point)) {
+ const { lng, lat } = point;
+ newItem.point = [lng, lat];
+ }
+ return (id >= 0 && iconName && Array.isArray(newItem.point)) ? newItem : null;
+ }).filter(Boolean);
+ if (!this._dataSource.length) {
+ console.warn('数据列表为空');
+ }
+ }
+
+ updateIcon = icon => {
+ if (!(icon instanceof Icon)) {
+ throw new Error(`入参icon必须是${Icon}的实例`);
+ }
+ this._icons = {
+ ...this._icons,
+ [icon.name]: icon,
+ };
+ };
+
+ _loadIcons() {
+ if (!this._map) return;
+ Object.values(this._icons).forEach(icon => {
+ if (this._map.hasImage(icon.name)) {
+ this._map.updateImage(icon.name, icon.data);
+ } else {
+ this._map.addImage(icon.name, icon.data);
+ }
+ });
+ }
+
+ _render() {
+ if (!this._map) return;
+ this._loadIcons();
+ this._renderPoints();
+ }
+
+ _renderPoints() {
+ const source = this._map.getSource(this._sourceId);
+ if (!source) {
+ this._map.addSource(this._sourceId, {
+ type: 'geojson',
+ data: this._pointFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId,
+ type: 'symbol',
+ source: this._sourceId,
+ paint: {
+ 'icon-opacity': [
+ 'case',
+ ['boolean', ['feature-state', 'visible'], true],
+ 1,
+ 0,
+ ],
+ // 'icon-translate': ['get', 'iconOffset'], // 缩放之后平移的像素值(mark:icon-translate不支持表达式)
+ },
+ layout: {
+ 'icon-image': ['get', 'iconName'],
+ 'icon-size': ['get', 'iconZoom'],
+ 'icon-offset': ['get', 'iconOffset'], // 乘以缩放值后,才是最终偏移量
+ 'icon-rotate': ['coalesce', ['get', 'rotate'], ['number', 0]],
+ 'icon-allow-overlap': true,
+ },
+ });
+
+ this._map.on('click', this._layerId, this._onClick);
+ this._map.on('mouseenter', this._layerId, this._onMouseEnter);
+ this._map.on('mousemove', this._layerId, this._onMouseMove);
+ this._map.on('mouseleave', this._layerId, this._onMouseLeave);
+ } else {
+ source.setData(this._pointFeatureCollection);
+ }
+ }
+
+ _onClick = e => {
+ if (!e.features.length) return;
+ const { lngLat, point } = e;
+ const [{ properties: detail }] = e.features;
+ this._trigger('click', { lngLat, point, detail });
+ };
+
+ _onMouseMove = () => {
+ this._map.getCanvas().style.cursor = 'pointer';
+ };
+
+ _onMouseEnter = e => {
+ if (!e.features.length) return;
+ const { lngLat, point } = e;
+ const [{ properties: detail }] = e.features;
+ this._trigger('mouseenter', { lngLat, point, detail });
+ };
+
+ _onMouseLeave = e => {
+ this._map.getCanvas().style.cursor = '';
+
+ const { lngLat, point } = e;
+ this._trigger('mouseleave', { lngLat, point });
+ };
+
+ // 是否包含某个点
+ hasPoint(pointId) {
+ return this._dataSource.findIndex(({ id }) => pointId === id) >= 0;
+ }
+
+ // 隐藏某个点
+ hidePoint(pointId) {
+ if (!this._map || !this.hasPoint(pointId)) return;
+ if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: false });
+ }
+
+ // 显示某个点
+ showPoint(pointId) {
+ if (!this._map || !this.hasPoint(pointId)) return;
+ if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: true });
+ }
+
+ changeVisibility(stateValue) {
+ if (!this._map) return;
+ if (this._map.getLayer(this._layerId)) {
+ this._map.setLayoutProperty(this._layerId, 'visibility', stateValue ? 'visible' : 'none');
+ }
+ }
+
+ clear() {
+ if (!this._map) return;
+ if (this._map.getSource(this._sourceId)) {
+ this._map.off('click', this._layerId, this._onClick);
+ this._map.off('mousemove', this._layerId, this._onMouseMove);
+ this._map.off('mouseleave', this._layerId, this._onMouseLeave);
+ this._map.off('mouseenter', this._layerId, this._onMouseEnter);
+ this._map.removeLayer(this._layerId);
+ this._map.removeSource(this._sourceId);
+ }
+ }
+
+ destroy() {
+ this.icons = {};
+ this._dataSource = [];
+ this.clear();
+ this._map = null;
+ }
+}
+
+export default IconPointRenderer;
diff --git a/src/core/Pinpoint.js b/src/core/Pinpoint.js
new file mode 100644
index 0000000..4380f15
--- /dev/null
+++ b/src/core/Pinpoint.js
@@ -0,0 +1,138 @@
+/**
+ * 位置点
+ */
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+
+const SOURCE_SYMBOL = 'pinpoint-source-symbol';
+const LAYER_SYMBOL = 'pinpoint-layer-symbol';
+
+class Pinpoint {
+ // 地图实例
+ map = null;
+
+ // 点坐标
+ coordinates = [];
+
+ // 操作点
+ pins = [];
+
+ // 视觉选项
+ option = {
+ mainColor: 'rgba(255,0,0,0.7)',
+ secondaryColor: '#fff',
+ shadowColor: 'rgba(255,0,0,0.2)',
+ fontSize: 12,
+ };
+
+ constructor(map, option = {}) {
+ this.map = map;
+ this.option = {
+ ...this.option,
+ ...option,
+ };
+ }
+
+ // 开启打点
+ onPinOn() {
+ this.map.getCanvas().style.cursor = 'crosshair';
+ this.coordinates = [];
+ this.pins = [];
+ this.map.on('click', this.onClickMap);
+ this.map.on('style.load', this.draw);
+ this.map.fire('pinpoint.on');
+ }
+
+ // 关闭打点
+ onPinOff() {
+ this.map.getCanvas().style.cursor = '';
+ if (this.map.getSource(SOURCE_SYMBOL)) {
+ this.map.removeLayer(LAYER_SYMBOL);
+ this.map.removeSource(SOURCE_SYMBOL);
+ }
+ this.pins.forEach(m => m.remove());
+ this.map.off('click', this.onClickMap);
+ this.map.off('style.load', this.draw);
+ this.map.fire('pinpoint.off');
+ }
+
+ // 生成点geojson
+ genPointFeature() {
+ const pointFeatures = this.coordinates.map(([lng, lat]) => turf.point([lng, lat], {
+ text: `${Number(lng).toFixed(7)}\n${Number(lat).toFixed(7)}`,
+ }));
+ return turf.featureCollection(pointFeatures);
+ }
+
+ draw = () => {
+ const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
+ if (!sourceSymbol) {
+ this.map.addSource(SOURCE_SYMBOL, {
+ type: 'geojson',
+ data: this.genPointFeature(),
+ });
+
+ this.map.addLayer({
+ id: LAYER_SYMBOL,
+ type: 'symbol',
+ source: SOURCE_SYMBOL,
+ layout: {
+ 'text-field': '{text}',
+ 'text-anchor': 'top',
+ 'text-justify': 'right',
+ 'text-size': this.option.fontSize,
+ 'text-offset': [0, 0.8],
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': this.option.mainColor,
+ 'text-halo-color': this.option.secondaryColor,
+ 'text-halo-width': 1,
+ },
+ });
+ } else {
+ sourceSymbol.setData(this.genPointFeature());
+ }
+ };
+
+ // 生成操作点
+ genPinNode() {
+ const node = document.createElement('div');
+ node.style.width = '17px';
+ node.style.height = '30px';
+ /* eslint-disable max-len */
+ node.innerHTML = `
`;
+ /* eslint-enable max-len */
+ return node;
+ }
+
+ onClickMap = e => {
+ const marker = new mapbox.Marker({
+ element: this.genPinNode(),
+ offset: [0, -12],
+ draggable: true,
+ }).setLngLat(e.lngLat).addTo(this.map);
+
+ const { lng, lat } = e.lngLat;
+ this.coordinates.push([lng, lat]);
+ this.draw();
+ this.pins.push(marker);
+ this.map.fire('pinpoint.change', { coordinates: this.coordinates });
+
+ marker.on('drag', () => {
+ const index = this.pins.indexOf(marker);
+ const { lng: newLng, lat: newLat } = marker.getLngLat();
+ this.coordinates[index] = [newLng, newLat];
+ this.draw();
+ });
+
+ marker.on('dragend', () => {
+ this.map.fire('pinpoint.change', { coordinates: this.coordinates });
+ });
+ };
+}
+
+export default Pinpoint;
diff --git a/src/core/Ruler.js b/src/core/Ruler.js
new file mode 100644
index 0000000..94c3478
--- /dev/null
+++ b/src/core/Ruler.js
@@ -0,0 +1,185 @@
+/**
+ * 测距尺
+ */
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+
+const SOURCE_LINE = 'ruler-source-line';
+const SOURCE_SYMBOL = 'ruler-source-symbol';
+const LAYER_LINE = 'ruler-layer-line';
+const LAYER_SYMBOL = 'ruler-layer-symbol';
+const labelFormat = n => (n < 1000 ? `${Number(n).toFixed(2)}米` : `${(n / 1000).toFixed(2)}千米`);
+
+class Ruler {
+ // 地图实例
+ map = null;
+
+ // 点坐标
+ coordinates = [];
+
+ // 距离值(含单位)
+ labels = [];
+
+ // 操作点
+ markers = [];
+
+ // 视觉选项
+ option = {
+ mainColor: 'rgba(255,0,0,0.8)',
+ secondaryColor: '#fff',
+ fontSize: 12,
+ };
+
+ constructor(map, option = {}) {
+ this.map = map;
+ this.option = {
+ ...this.option,
+ ...option,
+ };
+ }
+
+ // 开启测量(外部调用)
+ onMeasuringOn() {
+ this.map.getCanvas().style.cursor = 'crosshair';
+ this.coordinates = [];
+ this.markers = [];
+ this.labels = [];
+ this.map.on('click', this.onClickMap);
+ this.map.on('style.load', this.draw);
+ this.map.fire('ruler.on');
+ }
+
+ // 关闭测量(外部调用)
+ onMeasuringOff() {
+ this.map.getCanvas().style.cursor = '';
+ if (this.map.getSource(SOURCE_LINE)) {
+ this.map.removeLayer(LAYER_LINE);
+ this.map.removeSource(SOURCE_LINE);
+ }
+ if (this.map.getSource(SOURCE_SYMBOL)) {
+ this.map.removeLayer(LAYER_SYMBOL);
+ this.map.removeSource(SOURCE_SYMBOL);
+ }
+ this.markers.forEach(m => m.remove());
+ this.map.off('click', this.onClickMap);
+ this.map.off('style.load', this.draw);
+ this.map.fire('ruler.off');
+ }
+
+ // 生成线段geojson
+ genLineFeature() {
+ return turf.lineString(this.coordinates);
+ }
+
+ // 生成点geojson
+ genPointFeature() {
+ const pointFeatures = this.coordinates.map((coordinate, index) => turf.point(coordinate, {
+ text: this.labels[index],
+ }));
+ return turf.featureCollection(pointFeatures);
+ }
+
+ // 绘制
+ draw = () => {
+ if (this.coordinates.length >= 2) {
+ const sourceLine = this.map.getSource(SOURCE_LINE);
+ if (!sourceLine) {
+ this.map.addSource(SOURCE_LINE, {
+ type: 'geojson',
+ data: this.genLineFeature(),
+ });
+
+ this.map.addLayer({
+ id: LAYER_LINE,
+ type: 'line',
+ source: SOURCE_LINE,
+ paint: {
+ 'line-color': this.option.mainColor,
+ 'line-width': 2,
+ },
+ });
+ } else {
+ sourceLine.setData(this.genLineFeature());
+ }
+ }
+
+ const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
+ if (!sourceSymbol) {
+ this.map.addSource(SOURCE_SYMBOL, {
+ type: 'geojson',
+ data: this.genPointFeature(),
+ });
+
+ this.map.addLayer({
+ id: LAYER_SYMBOL,
+ type: 'symbol',
+ source: SOURCE_SYMBOL,
+ layout: {
+ 'text-field': '{text}',
+ 'text-anchor': 'top',
+ 'text-size': this.option.fontSize,
+ 'text-offset': [0, 0.8],
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': this.option.mainColor,
+ 'text-halo-color': this.option.secondaryColor,
+ 'text-halo-width': 1,
+ },
+ });
+ } else {
+ sourceSymbol.setData(this.genPointFeature());
+ }
+ };
+
+ // 生成操作点
+ genMarkerNode() {
+ const node = document.createElement('div');
+ node.style.width = '12px';
+ node.style.height = '12px';
+ node.style.borderRadius = '50%';
+ node.style.background = this.option.secondaryColor;
+ node.style.boxSizing = 'border-box';
+ node.style.border = `2px solid ${this.option.mainColor}`;
+ return node;
+ }
+
+ onClickMap = e => {
+ const marker = new mapbox.Marker({
+ element: this.genMarkerNode(),
+ draggable: true,
+ }).setLngLat(e.lngLat).addTo(this.map);
+
+ const { lng, lat } = e.lngLat;
+ this.coordinates.push([lng, lat]);
+ this.updateLabels();
+ this.draw();
+ this.markers.push(marker);
+ this.map.fire('ruler.change', { coordinates: this.coordinates });
+
+ marker.on('drag', () => {
+ const index = this.markers.indexOf(marker);
+ const { lng: newLng, lat: newLat } = marker.getLngLat();
+ this.coordinates[index] = [newLng, newLat];
+ this.updateLabels();
+ this.draw();
+ });
+
+ marker.on('dragend', () => {
+ this.map.fire('ruler.change', { coordinates: this.coordinates });
+ });
+ };
+
+ // 更新测量结果值
+ updateLabels() {
+ const { coordinates } = this;
+ let sum = 0;
+ this.labels = coordinates.map((coordinate, index) => {
+ if (index === 0) return labelFormat(0);
+ sum += turf.distance(coordinates[index - 1], coordinates[index], { units: 'meters' });
+ return labelFormat(sum);
+ });
+ }
+}
+
+export default Ruler;
diff --git a/src/core/ShapeGroupRenderer.js b/src/core/ShapeGroupRenderer.js
new file mode 100644
index 0000000..47b33fb
--- /dev/null
+++ b/src/core/ShapeGroupRenderer.js
@@ -0,0 +1,455 @@
+/**
+ * 多边形组显示
+ */
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+
+class ShapeGroupRenderer {
+ _topic = null;
+
+ // 地图实例
+ _map = null;
+
+ // 分组标识
+ _groupIdKey = null;
+
+ // 分组名称标识
+ _groupNameKey = null;
+
+ // 分组父级标识
+ _groupParentIdKey = null;
+
+ _dataSource = [];
+
+ _defaultOptions = {
+ // 显示描边
+ showStroke: true,
+ // 显示标签
+ showLabel: true,
+ // 显示填充
+ showFill: true,
+ };
+
+ _options = {};
+
+ _defaultStyle = {
+ color: 'rgba(255, 95, 0, 0.7)',
+ hoverColor: 'rgba(255, 95, 0, 1)',
+ offset: -2,
+ isDashed: false,
+ labelColor: 'rgba(255, 95, 0, 1)',
+ labelStrokeColor: 'rgba(0, 0, 0, 0.4)',
+ labelMinZoom: 3,
+ };
+
+ // 视觉选项
+ _style = {};
+
+ get _sourceId() {
+ return {
+ FILL: `${this._topic}-shape-fill-source`,
+ STROKE: `${this._topic}-shape-hull-stroke-source`,
+ LABEL: `${this._topic}-shape-hull-label-source`,
+ };
+ }
+
+ get _layerId() {
+ return {
+ FILL: `${this._topic}-shape-fill-source`,
+ STROKE: `${this._topic}-shape-hull-stroke-layer`,
+ LABEL: `${this._topic}-shape-hull-label-layer`,
+ };
+ }
+
+ // 形状填充
+ get _shapeFillFeatures() {
+ return this._dataSource.map(({ id, points, ...others }) => {
+ const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
+ const lineFeature = turf.lineString(newPoints, { id, ...others });
+ const polygonFeature = turf.lineToPolygon(lineFeature);
+ polygonFeature.id = id;
+ return polygonFeature;
+ });
+ }
+
+ // 形状填充集合
+ get _shapeFillFeatureCollection() {
+ return turf.featureCollection(this._shapeFillFeatures);
+ }
+
+ // 形状填充色表达式
+ get _shapeFillColorExpression() {
+ return [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ this._options.showFill ? this._style.fillHoverColor : 'rgba(0, 0, 0, 0)',
+ this._options.showFill ? this._style.fillColor : 'rgba(0, 0, 0, 0)',
+ ];
+ }
+
+ // 边框颜色表达式
+ get _strokeColorExpression() {
+ return [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ this._style.hoverColor,
+ this._style.color,
+ ];
+ }
+
+ get _strokeDashedExpression() {
+ return this._style.isDashed ? [2, 2] : [1];
+ }
+
+ // 标签透明度表达式
+ get _labelOpacityExpression() {
+ return [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._style.labelMinZoom - 0.01, 0,
+ this._style.labelMinZoom, 1,
+ ];
+ }
+
+ // 分组数据
+ get _group() {
+ const group = {};
+ Object.values(this._dataSource).forEach(item => {
+ const { [this._groupIdKey]: groupId } = item;
+ if (groupId) {
+ group[groupId] = [
+ ...(group[groupId] || []),
+ item,
+ ];
+ }
+ });
+ return group;
+ }
+
+ // 每个组中所有形状边界点集合
+ get _pointFeatureCollections() {
+ const group = {};
+ Object.keys(this._group).forEach(groupId => {
+ const groupPoints = this._group[groupId].map(({ points }) => points).flat();
+ const pointFeatures = groupPoints.map(point => turf.point(point));
+ group[groupId] = turf.featureCollection(pointFeatures);
+ });
+ return group;
+ }
+
+ // 每个组的凸包
+ get _convexHulls() {
+ return Object.keys(this._pointFeatureCollections).map(groupId => {
+ const convexHull = turf.convex(this._pointFeatureCollections[groupId]);
+ const [{ [this._groupNameKey]: groupName, [this._groupParentIdKey]: groupParentId }] = this._group[groupId];
+ convexHull.id = groupId - 0;
+ convexHull.properties = {
+ [this._groupIdKey]: groupId - 0,
+ [this._groupNameKey]: groupName || '',
+ [this._groupParentIdKey]: (groupParentId - 0) || 0,
+ };
+ return convexHull;
+ });
+ }
+
+ // 组凸包集合
+ get _convexHullFeatureCollection() {
+ return turf.featureCollection(this._convexHulls);
+ }
+
+ // 各分组中心点
+ get _groupCenterFeatures() {
+ return this._convexHulls.map(feature => {
+ const pointFeature = turf.centerOfMass(feature);
+ pointFeature.properties = feature.properties;
+ return pointFeature;
+ });
+ }
+
+ // 分组中心点集合
+ get _groupCenterFeatureCollection() {
+ return turf.featureCollection(this._groupCenterFeatures);
+ }
+
+ constructor(topic, groupIdKey, groupNameKey = null, groupParentIdKey = null) {
+ this._topic = topic;
+ this._groupIdKey = groupIdKey;
+ this._groupNameKey = groupNameKey;
+ this._groupParentIdKey = groupParentIdKey;
+
+ this.updateOptions();
+ this.updateStyle();
+
+ makeObservable(this, {
+ _topic: observable,
+ _dataSource: observable,
+ _options: observable,
+ _style: observable,
+ _sourceId: computed,
+ _layerId: computed,
+ _strokeColorExpression: computed,
+ _strokeDashedExpression: computed,
+ _labelOpacityExpression: computed,
+ _group: computed,
+ _pointFeatureCollections: computed,
+ _convexHulls: computed,
+ _convexHullFeatureCollection: computed,
+ _groupCenterFeatures: computed,
+ _groupCenterFeatureCollection: computed,
+ loadDataSource: action,
+ updateOptions: action,
+ updateStyle: action,
+ destroy: action,
+ });
+
+ reaction(() => this._dataSource, () => {
+ this._render();
+ });
+ }
+
+ // 更新配置项
+ updateOptions(options = {}) {
+ if (Object.keys(options).length) {
+ this._options = {
+ ...this._options,
+ ...options,
+ };
+ this._refreshVisibility();
+ } else {
+ this._options = {
+ ...this._defaultOptions,
+ };
+ }
+ }
+
+ // 更新视觉样式
+ updateStyle(style = {}) {
+ if (Object.keys(style).length) {
+ this._style = {
+ ...this._style,
+ ...style,
+ };
+ this._repaintStyle();
+ } else {
+ this._style = {
+ ...this._defaultStyle,
+ };
+ }
+ }
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ // 载入数据(list->item必须包含id、%_groupIdKey%、points数组,points->item如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
+ loadDataSource(list) {
+ if (!this._map) {
+ throw new Error('请先设置地图实例');
+ }
+ this._dataSource = (list || []).map(item => {
+ const { id, points } = item;
+ const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
+ return (id >= 0 && Array.isArray(points) && points.length >= 3) ? { ...item, points: newPoints } : null;
+ }).filter(Boolean);
+ }
+
+ _render() {
+ if (!this._map) return;
+ if (this._options.showStroke) this._renderStroke();
+ if (this._options.showLabel) this._renderLabel();
+ if (this._options.showFill) this._renderShapeFill();
+ }
+
+ // 渲染形状填充
+ _renderShapeFill() {
+ const source = this._map.getSource(this._sourceId.FILL);
+
+ if (!source) {
+ this._map.addSource(this._sourceId.FILL, {
+ type: 'geojson',
+ data: this._shapeFillFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.FILL,
+ type: 'fill',
+ source: this._sourceId.FILL,
+ paint: {
+ 'fill-color': this._shapeFillColorExpression,
+ 'fill-opacity': [
+ 'case',
+ ['boolean', ['feature-state', 'visible'], true],
+ 1,
+ 0,
+ ],
+ },
+ });
+
+ // this._map.on('click', this._layerId.FILL, this._onClick);
+ // this._map.on('mousemove', this._layerId.FILL, this._onMouseMove);
+ // this._map.on('mouseleave', this._layerId.FILL, this._onMouseLeave);
+ } else {
+ source.setData(this._shapeFillFeatureCollection);
+ }
+ }
+
+ _renderStroke() {
+ const source = this._map.getSource(this._sourceId.STROKE);
+ if (!source) {
+ this._map.addSource(this._sourceId.STROKE, {
+ type: 'geojson',
+ data: this._convexHullFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.STROKE,
+ type: 'line',
+ source: this._sourceId.STROKE,
+ paint: {
+ 'line-color': this._strokeColorExpression,
+ 'line-width': [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ 2,
+ 1,
+ ],
+ 'line-dasharray': this._strokeDashedExpression,
+ 'line-offset': this._defaultStyle.offset,
+ },
+ });
+ } else {
+ source.setData(this._convexHullFeatureCollection);
+ }
+ }
+
+ _renderLabel() {
+ const source = this._map.getSource(this._sourceId.LABEL);
+ if (!source) {
+ this._map.addSource(this._sourceId.LABEL, {
+ type: 'geojson',
+ data: this._groupCenterFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.LABEL,
+ type: 'symbol',
+ source: this._sourceId.LABEL,
+ layout: {
+ 'text-field': `{${this._groupNameKey}}`,
+ 'text-size': 12,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': this._style.labelColor,
+ 'text-halo-color': this._style.labelStrokeColor,
+ 'text-halo-width': 1,
+ 'text-translate': [0, -20],
+ 'text-opacity': this._labelOpacityExpression,
+ },
+ });
+ } else {
+ source.setData(this._groupCenterFeatureCollection);
+ }
+ }
+
+ // 高亮某个分组框(或取消高亮)
+ highlightGroup(groupId, stateValue = true) {
+ if (!this._map) return;
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: groupId }, { highlight: stateValue });
+ if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id: groupId }, { highlight: stateValue });
+ }
+
+ // 高亮一组分组框(或取消高亮)
+ highlightGroups(keyName, keyValue, stateValue = true) {
+ if (!this._map) return;
+ turf.featureEach(this._convexHullFeatureCollection, currentFeature => {
+ const { id } = currentFeature;
+ if (currentFeature.properties[keyName] === keyValue) this.highlightGroup(id, stateValue);
+ });
+ }
+
+ _refreshVisibility() {
+ if (!this._map) return;
+ if (this._map.getLayer(this._layerId.STROKE)) this._map.setLayoutProperty(this._layerId.STROKE, 'visibility', this._options.showStroke ? 'visible' : 'none');
+ if (this._map.getLayer(this._layerId.LABEL)) this._map.setLayoutProperty(this._layerId.LABEL, 'visibility', this._options.showLabel ? 'visible' : 'none');
+ }
+
+ _repaintStyle() {
+ if (!this._map) return;
+ if (this._map.getLayer(this._layerId.FILL)) {
+ this._map.setPaintProperty(this._layerId.FILL, 'fill-color', this._shapeFillColorExpression);
+ }
+ if (this._map.getLayer(this._layerId.STROKE)) {
+ this._map.setPaintProperty(this._layerId.STROKE, 'line-color', this._strokeColorExpression);
+ this._map.setPaintProperty(this._layerId.STROKE, 'line-dasharray', this._strokeDashedExpression);
+ this._map.setPaintProperty(this._layerId.STROKE, 'line-offset', this._defaultStyle.offset);
+ }
+ if (this._map.getLayer(this._layerId.LABEL)) {
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-color', this._style.labelColor);
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-halo-color', this._style.labelStrokeColor);
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-opacity', this._labelOpacityExpression);
+ }
+ }
+
+ // 缩放到包围盒
+ fit(groupId, { top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
+ if (!this._map || !(groupId in this._group)) return;
+ const feature = this._convexHullFeatureCollection.features.find(({ id }) => id === groupId);
+ if (!feature) return;
+ this._map.fitBounds(turf.bbox(feature), {
+ duration: 2000,
+ padding: {
+ top: top + cut,
+ bottom: bottom + cut,
+ left: left + cut,
+ right: right + cut,
+ },
+ });
+ }
+
+ clear() {
+ if (!this._map) return;
+ if (this._map.getSource(this._sourceId.STROKE)) {
+ this._map.removeLayer(this._layerId.STROKE);
+ this._map.removeSource(this._sourceId.STROKE);
+ }
+ if (this._map.getSource(this._sourceId.LABEL)) {
+ this._map.removeLayer(this._layerId.LABEL);
+ this._map.removeSource(this._sourceId.LABEL);
+ }
+ if (this._map.getSource(this._sourceId.FILL)) {
+ // this._map.off('click', this._layerId.FILL, this._onClick);
+ // this._map.off('mousemove', this._layerId.FILL, this._onMouseMove);
+ // this._map.off('mouseleave', this._layerId.FILL, this._onMouseLeave);
+ this._map.removeLayer(this._layerId.FILL);
+ this._map.removeSource(this._sourceId.FILL);
+ }
+ }
+
+ destroy() {
+ this.clear();
+ this._map = null;
+ this._dataSource = [];
+ this.updateStyle();
+ this.updateOptions();
+ }
+}
+
+export default ShapeGroupRenderer;
diff --git a/src/core/blockHelper/BlockHelper.js b/src/core/blockHelper/BlockHelper.js
new file mode 100644
index 0000000..7b5ad06
--- /dev/null
+++ b/src/core/blockHelper/BlockHelper.js
@@ -0,0 +1,63 @@
+/**
+ * 地块助手
+ */
+import blockRenderer from './blockRenderer';
+import groupRenderer from './groupRenderer';
+import subFarmRenderer from './subFarmRenderer';
+
+class BlockHelper {
+ static blockRenderer = blockRenderer;
+
+ static groupRenderer = groupRenderer;
+
+ static subFarmRenderer = subFarmRenderer;
+
+ static setMap(map) {
+ blockRenderer.setMap(map);
+ groupRenderer.setMap(map);
+ subFarmRenderer.setMap(map);
+ }
+
+ static loadDataSource(list = []) {
+ blockRenderer.loadDataSource(list);
+ groupRenderer.loadDataSource(list);
+ subFarmRenderer.loadDataSource(list);
+ }
+
+ static getBlock(blockId) {
+ return blockRenderer.getShape(blockId - 0);
+ }
+
+ static highlightBlock(blockId, stateValue) {
+ blockRenderer.highlightShape(blockId - 0, stateValue);
+ }
+
+ static highlightGroup(groupId, stateValue) {
+ groupRenderer.highlightGroup(groupId - 0, stateValue);
+ blockRenderer.highlightShapes('groupId', groupId - 0, stateValue);
+ }
+
+ static highlightSubFarm(subFarmId, stateValue) {
+ subFarmRenderer.highlightGroup(subFarmId - 0, stateValue);
+ groupRenderer.highlightGroups('subFarmId', subFarmId - 0, stateValue);
+ blockRenderer.highlightShapes('subFarmId', subFarmId - 0, stateValue);
+ }
+
+ static fitBlock(blockId, padding = {}, cut = 120) {
+ blockRenderer.fitShape(blockId - 0, padding, cut);
+ }
+
+ static fitGroup(groupId, padding = {}, cut = 120) {
+ groupRenderer.fit(groupId - 0, padding, cut);
+ }
+
+ static fitSubFarm(subFarmId, padding = {}, cut = 120) {
+ subFarmRenderer.fit(subFarmId - 0, padding, cut);
+ }
+
+ static fitView(padding = {}, cut = 120) {
+ blockRenderer.fitView(padding, cut);
+ }
+}
+
+export default BlockHelper;
diff --git a/src/core/blockHelper/blockRenderer.js b/src/core/blockHelper/blockRenderer.js
new file mode 100644
index 0000000..6ec7c10
--- /dev/null
+++ b/src/core/blockHelper/blockRenderer.js
@@ -0,0 +1,23 @@
+/**
+ * 地块显示
+ */
+import ShapeRenderer from '../ShapeRenderer';
+
+class BlockRenderer extends ShapeRenderer {
+ _defaultStyle = {
+ fillColor: 'rgba(255, 255, 0, 0.1)',
+ fillHoverColor: 'rgba(255, 255, 0, 0.3)',
+ strokeColor: 'rgba(255, 255, 0, 0.5)',
+ strokeHoverColor: 'rgba(255, 255, 0, 1)',
+ labelColor: 'rgba(0, 0, 0, 1)',
+ labelStrokeColor: 'rgba(255, 255, 255, 1)',
+ labelMinZoom: 16,
+ };
+
+ constructor() {
+ super('block');
+ this.updateStyle();
+ }
+}
+
+export default new BlockRenderer();
diff --git a/src/core/blockHelper/groupRenderer.js b/src/core/blockHelper/groupRenderer.js
new file mode 100644
index 0000000..acf4975
--- /dev/null
+++ b/src/core/blockHelper/groupRenderer.js
@@ -0,0 +1,23 @@
+/**
+ * 地块分组显示
+ */
+import ShapeGroupRenderer from '../ShapeGroupRenderer';
+
+class GroupRenderer extends ShapeGroupRenderer {
+ _defaultStyle = {
+ color: 'rgba(255, 95, 0, 0.7)',
+ hoverColor: 'rgba(255, 95, 0, 1)',
+ offset: -2,
+ isDashed: true,
+ labelColor: 'rgba(255, 95, 0, 1)',
+ labelStrokeColor: 'rgba(0, 0, 0, 0.4)',
+ labelMinZoom: 14,
+ };
+
+ constructor() {
+ super('blockGroup', 'groupId', 'groupName', 'subFarmId');
+ this.updateStyle();
+ }
+}
+
+export default new GroupRenderer();
diff --git a/src/core/blockHelper/subFarmRenderer.js b/src/core/blockHelper/subFarmRenderer.js
new file mode 100644
index 0000000..ac6b57f
--- /dev/null
+++ b/src/core/blockHelper/subFarmRenderer.js
@@ -0,0 +1,26 @@
+/**
+ * 地块分组显示
+ */
+import ShapeGroupRenderer from '../ShapeGroupRenderer';
+
+class SubFarmRenderer extends ShapeGroupRenderer {
+ _defaultOptions = {
+ showStroke: true,
+ showLabel: false,
+ };
+
+ _defaultStyle = {
+ color: 'rgba(170, 0, 255, 0.7)',
+ hoverColor: 'rgba(170, 0, 255, 1)',
+ offset: -5,
+ isDashed: false,
+ };
+
+ constructor() {
+ super('blockSubFarm', 'subFarmId', 'subFarmName', 'farmId');
+ this.updateOptions();
+ this.updateStyle();
+ }
+}
+
+export default new SubFarmRenderer();
diff --git a/src/core/clusterControlHelper.js b/src/core/clusterControlHelper.js
new file mode 100644
index 0000000..50c6a91
--- /dev/null
+++ b/src/core/clusterControlHelper.js
@@ -0,0 +1,356 @@
+/**
+ * 集群控制助手
+ * 分段显示地块边+选择边+显示航线轨迹
+ */
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import chroma from 'chroma-js';
+
+const SOURCE_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-source';
+const LAYER_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-layer';
+const SOURCE_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-source';
+const LAYER_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-layer';
+const SOURCE_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-source';
+const LAYER_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-layer';
+const SOURCE_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-source';
+const LAYER_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-layer';
+
+class ClusterControlHelper {
+ // 地图实例
+ map = null;
+
+ // 单个地块数据
+ dataSource = {};
+
+ // 多条轨迹
+ tracks = [];
+
+ // 鼠标经过的边索引
+ hoveredEdgeIndex = null;
+
+ // 选边回调
+ pickEdgeCallback = () => {};
+
+ // 地块各边集合
+ get edgeLineFeatureCollection() {
+ const { points } = this.dataSource;
+ if (!Array.isArray(points) || !points.length) {
+ return turf.featureCollection([]);
+ }
+ const boundaryFeature = turf.lineString(points);
+ return turf.lineSegment(turf.lineToPolygon(boundaryFeature));
+ }
+
+ // 每条边中点
+ get edgeLineMiddlePointFeatureCollection() {
+ const points = [];
+ turf.featureEach(this.edgeLineFeatureCollection, (feature, index) => {
+ const [point1, point2] = turf.getCoords(feature).map(position => turf.point(position));
+ const point = turf.midpoint(point1, point2);
+ point.id = index;
+ point.properties = { n: index + 1 };
+ points.push(point);
+ });
+ return turf.featureCollection(points);
+ }
+
+ // 航线颜色
+ get trackColors() {
+ return chroma.scale([
+ chroma.hsl(0, 1, 0.4),
+ chroma.hsl(30, 1, 0.4),
+ chroma.hsl(60, 1, 0.4),
+ chroma.hsl(90, 1, 0.4),
+ chroma.hsl(120, 1, 0.4),
+ chroma.hsl(150, 1, 0.4),
+ chroma.hsl(180, 1, 0.4),
+ chroma.hsl(210, 1, 0.4),
+ chroma.hsl(240, 1, 0.4),
+ chroma.hsl(270, 1, 0.4),
+ chroma.hsl(300, 1, 0.4),
+ chroma.hsl(330, 1, 0.4),
+ chroma.hsl(360, 1, 0.4),
+ ]).colors(this.tracks.length + 1);
+ }
+
+ // 航线
+ get trackLineFeatureCollection() {
+ const features = this.tracks.map((track, index) => {
+ const color = this.trackColors[index];
+ return turf.lineString(track, { color }, { id: index });
+ });
+ return turf.featureCollection(features);
+ }
+
+ // 航线编号
+ get trackLabelFeatureCollection() {
+ const showNumber = this.tracks.length > 1;
+ const features = this.tracks.map((track, index) => {
+ const color = this.trackColors[index];
+ const number = showNumber ? index + 1 : '';
+ const [point] = track;
+ return turf.point(point, { color, number }, { id: index });
+ });
+ return turf.featureCollection(features);
+ }
+
+ constructor() {
+ makeObservable(this, {
+ dataSource: observable,
+ tracks: observable,
+ edgeLineFeatureCollection: computed,
+ edgeLineMiddlePointFeatureCollection: computed,
+ trackColors: computed,
+ trackLineFeatureCollection: computed,
+ trackLabelFeatureCollection: computed,
+ loadBlock: action,
+ loadTracks: action,
+ destroy: action,
+ });
+
+ reaction(() => this.dataSource, () => {
+ this.render();
+ });
+ reaction(() => this.tracks, () => {
+ this.renderTracks();
+ });
+ }
+
+ // 设置地图实例
+ setMap(map) {
+ if (this.map === map) return;
+ if (!(map instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this.map = map;
+ }
+
+ // 载入地块详情
+ loadBlock(detail) {
+ if (!this.map) {
+ throw new Error('请先设置地图实例');
+ }
+ const { points, ...otherDetail } = detail;
+ const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
+ this.dataSource = {
+ ...otherDetail,
+ points: newPoints,
+ };
+ }
+
+ // 载入多条轨迹
+ loadTracks(tracks) {
+ this.tracks = (tracks || []).map(track => (track || []).map(({ longitude, latitude }) => [longitude, latitude]));
+ }
+
+ render() {
+ if (!this.map) return;
+ this.renderEdgeLine();
+ this.renderEdgeLabel();
+ }
+
+ // 边界线
+ renderEdgeLine() {
+ const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE);
+ if (!source) {
+ this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE, {
+ type: 'geojson',
+ data: this.edgeLineFeatureCollection,
+ });
+
+ this.map.addLayer({
+ id: LAYER_CLUSTER_CONTROL_EDGE_LINE,
+ type: 'line',
+ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE,
+ layout: {
+ 'line-cap': 'round',
+ },
+ paint: {
+ 'line-color': [
+ 'case',
+ ['boolean', ['feature-state', 'hover'], false],
+ 'rgba(0,0,0,1)',
+ 'rgba(0,0,0,0.5)',
+ ],
+ 'line-width': 6,
+ },
+ });
+
+ this.map.on('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge);
+ this.map.on('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge);
+ this.map.on('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge);
+ } else {
+ source.setData(this.edgeLineFeatureCollection);
+ }
+ }
+
+ renderEdgeLabel() {
+ const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL);
+ if (!source) {
+ this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL, {
+ type: 'geojson',
+ data: this.edgeLineMiddlePointFeatureCollection,
+ });
+
+ this.map.addLayer({
+ id: LAYER_CLUSTER_CONTROL_EDGE_LABEL,
+ type: 'symbol',
+ source: SOURCE_CLUSTER_CONTROL_EDGE_LABEL,
+ layout: {
+ 'text-field': '{n}',
+ 'text-size': 16,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': 'white',
+ 'text-halo-color': 'black',
+ 'text-halo-width': 2,
+ },
+ });
+ } else {
+ source.setData(this.edgeLineMiddlePointFeatureCollection);
+ }
+ }
+
+ renderTracks() {
+ if (!this.map) return;
+ // 轨迹线
+ {
+ const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE);
+ if (!source) {
+ this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE, {
+ type: 'geojson',
+ data: this.trackLineFeatureCollection,
+ });
+
+ this.map.addLayer({
+ id: LAYER_CLUSTER_CONTROL_TRACK_LINE,
+ type: 'line',
+ source: SOURCE_CLUSTER_CONTROL_TRACK_LINE,
+ layout: {
+ 'line-cap': 'round',
+ },
+ paint: {
+ 'line-color': ['get', 'color'],
+ 'line-width': 2,
+ },
+ });
+ } else {
+ this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE).setData(this.trackLineFeatureCollection);
+ this.fadeTracks(1);
+ }
+ }
+ // 轨迹编号
+ {
+ const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL);
+ if (!source) {
+ this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL, {
+ type: 'geojson',
+ data: this.trackLabelFeatureCollection,
+ });
+
+ this.map.addLayer({
+ id: LAYER_CLUSTER_CONTROL_TRACK_LABEL,
+ type: 'symbol',
+ source: SOURCE_CLUSTER_CONTROL_TRACK_LABEL,
+ layout: {
+ 'text-field': ['get', 'number'],
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': ['get', 'color'],
+ 'text-halo-width': 1,
+ 'text-halo-color': 'black',
+ },
+ });
+ } else {
+ this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL).setData(this.trackLabelFeatureCollection);
+ }
+ }
+ }
+
+ // 淡化
+ fadeTracks(opacity) {
+ if (!this.map) return;
+ if (this.map.getLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE)) {
+ this.map.setPaintProperty(LAYER_CLUSTER_CONTROL_TRACK_LINE, 'line-opacity', opacity);
+ }
+ }
+
+ onEnterEdge = e => {
+ if (!e.features.length) return;
+ this.map.getCanvas().style.cursor = 'pointer';
+ if (this.hoveredEdgeIndex !== null) {
+ this.map.setFeatureState(
+ { source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
+ { hover: false },
+ );
+ }
+ this.hoveredEdgeIndex = e.features[0].id;
+ this.map.setFeatureState(
+ { source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
+ { hover: true },
+ );
+ };
+
+ onLeaveEdge = () => {
+ this.map.getCanvas().style.cursor = '';
+ if (this.hoveredEdgeIndex !== null) {
+ this.map.setFeatureState(
+ { source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
+ { hover: false },
+ );
+ }
+ this.hoveredEdgeIndex = null;
+ };
+
+ onPickEdge = e => {
+ const [{ id } = {}] = e.features;
+ this.pickEdgeCallback(id);
+ };
+
+ // 清除边
+ clearEdge() {
+ if (!this.map) return;
+ if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE)) {
+ this.map.off('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge);
+ this.map.off('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge);
+ this.map.off('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge);
+ this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LINE);
+ this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE);
+ }
+ if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL)) {
+ this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LABEL);
+ this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL);
+ }
+ }
+
+ // 清除航线
+ clearTrack() {
+ if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE)) {
+ this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE);
+ this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE);
+ }
+ if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL)) {
+ this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LABEL);
+ this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL);
+ }
+ }
+
+ clearAll() {
+ this.clearEdge();
+ this.clearTrack();
+ }
+
+ destroy() {
+ this.clearAll();
+ this.pickEdgeCallback = () => {};
+ this.hoveredEdgeIndex = null;
+ this.tracks = [];
+ this.dataSource = {};
+ this.map = null;
+ }
+}
+
+export default new ClusterControlHelper();
diff --git a/src/core/deviceCruise.js b/src/core/deviceCruise.js
new file mode 100644
index 0000000..3bff09d
--- /dev/null
+++ b/src/core/deviceCruise.js
@@ -0,0 +1,308 @@
+/**
+ * 设备按轨迹巡航
+ */
+// import Vue from 'vue';
+import { reactive } from 'vue';
+import { interpolate } from 'popmotion';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import chroma from 'chroma-js';
+import Icon from './IconPoint/Icon';
+import DroneIcon from '../assets/DroneIcon';
+
+const SOURCE_DEVICE_CRUISE_POINT = 'device-cruise-point-source';
+const LAYER_DEVICE_CRUISE_POINT = 'device-cruise-point-layer';
+const DEVICE_CRUISE_POINT_ICON = 'device-cruise-point-icon';
+
+class DeviceCruise {
+ _that = null;
+
+ // 地图实例
+ _map = null;
+
+ _icon = null;
+
+ // 单条轨迹数据
+ _dataSource = {};
+
+ // 巡航速率
+ speedRate = 1;
+
+ // 动画计时器
+ _timer = null;
+
+ // 上一帧时间点
+ _lastFrameAt = 0;
+
+ // 累计播放时长(毫秒数)
+ elapsedMs = 0;
+
+ // 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
+ _fpsInterval = 1000 / 30;
+
+ // 上一帧时间戳
+ _lastFrameTimestamp = 0;
+
+ // 巡航到的时间点数据
+ timelyData = {};
+
+ isPlaying = false;
+
+ isPaused = false;
+
+ isStopped = true;
+
+ // 视觉样式
+ _defaultStyle = {
+ deviceMainColor: '#1890ff',
+ deviceDirectionColor: 'rgba(255,255,0,0.75)',
+ };
+
+ _style = {};
+
+ // 是否准备完毕
+ get ready() {
+ return Object.keys(this._that._dataSource).length > 0;
+ }
+
+ get _points() {
+ const { points } = this._that._dataSource;
+ return points || [];
+ }
+
+ // 基准时间点(从0开始的毫秒数)
+ get _datumTime() {
+ const [{ timestamp: startTs } = {}] = this._that._points || [];
+ return this._that._points.map(({ timestamp }) => timestamp - startTs);
+ }
+
+ // 轨迹总时间(毫秒数)
+ get totalTime() {
+ return this._that._datumTime[this._that._datumTime.length - 1] || 0;
+ }
+
+ // 是否已经开始播放了
+ get isStarted() {
+ return this._that.isPlaying || this._that.isPaused;
+ }
+
+ constructor() {
+ this._that = reactive(this);
+ // Vue.observable(this);
+ this._that.initStyle();
+ // eslint-disable-next-line no-constructor-return
+ return this._that;
+ }
+
+ // 初始化视觉演示(需在loadTracks之前配置)
+ initStyle(style = {}) {
+ this._that._style = {
+ ...this._that._defaultStyle,
+ ...style,
+ };
+ }
+
+ // 设置地图实例
+ setMap(mapInstance) {
+ if (this._that._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._that._map = mapInstance;
+ }
+
+ setIcon(icon) {
+ if (!(icon instanceof Icon)) {
+ throw new Error(`入参icon必须是${Icon}的实例`);
+ }
+ this._that._icon = icon;
+ if (this._that._map.hasImage(this._that._icon.name)) {
+ this._that._map.updateImage(this._that._icon.name, this._that._icon.data);
+ } else {
+ this._that._map.addImage(this._that._icon.name, this._that._icon.data);
+ }
+ }
+
+ // 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
+ loadTrack({ id, points, ...others }) {
+ if (!this._that._map) {
+ throw new Error('请先设置地图实例');
+ }
+ this._that._dataSource = {
+ id,
+ points: (points || []).map(item => ({ ...item })),
+ ...others,
+ };
+ this._that._reset();
+ this._that._initDevice();
+ }
+
+ // 设置速率
+ setSpeedRate(val) {
+ this._that.speedRate = val;
+ }
+
+ // 设置当前巡航时间点
+ setCurrentTime(ms) {
+ if (ms < 0 || ms > this._that.totalTime) return;
+ this._that.elapsedMs = ms;
+ // if (!this._that.isStarted) {
+ this._that._renderDevice();
+ // }
+ }
+
+ // 获取指定毫秒处的数据值
+ _getTimelyData(ms = 0) {
+ const genTimelyData = interpolate(this._that._datumTime, this._that._points);
+ return genTimelyData(ms);
+ }
+
+ _reset() {
+ this._that.isPlaying = false;
+ this._that.isPaused = false;
+ this._that.isStopped = true;
+ this._that.speedRate = 1;
+ this._that._lastFrameAt = 0;
+ this._that.elapsedMs = 0;
+ this._that._lastFrameTimestamp = 0;
+ }
+
+ _initDevice() {
+ if (!this._that._icon) {
+ const icon = new Icon(DEVICE_CRUISE_POINT_ICON, new DroneIcon({
+ outlineColor: this._that._style.deviceMainColor,
+ bodyColor: chroma(this._that._style.deviceMainColor).alpha(0.5).css(),
+ directionColor: this._that._style.deviceDirectionColor,
+ }), 0.25);
+ this._that.setIcon(icon);
+ }
+
+ const [point] = this._that._points;
+ const { lng, lat, yaw } = point || {};
+ const pointFeature = point ? turf.point([lng, lat], {
+ ...point,
+ yaw: (+Number(yaw) || 0).toFixed(2),
+ }) : turf.multiPoint([]);
+
+ const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
+ if (!source) {
+ this._that._map.addSource(SOURCE_DEVICE_CRUISE_POINT, {
+ type: 'geojson',
+ data: pointFeature,
+ });
+
+ this._that._map.addLayer({
+ id: LAYER_DEVICE_CRUISE_POINT,
+ type: 'symbol',
+ source: SOURCE_DEVICE_CRUISE_POINT,
+ layout: {
+ 'icon-image': this._that._icon.name,
+ 'icon-size': this._that._icon.zoom,
+ 'icon-offset': this._that._icon.offset,
+ 'icon-rotate': ['get', 'yaw'],
+ 'icon-allow-overlap': true,
+ },
+ });
+ } else {
+ source.setData(pointFeature);
+ this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-image', this._that._icon.name);
+ this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-size', this._that._icon.zoom);
+ this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-offset', this._that._icon.offset);
+ }
+ }
+
+ _clearDevice() {
+ if (!this._that._map) return;
+ if (this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT)) {
+ this._that._map.removeLayer(LAYER_DEVICE_CRUISE_POINT);
+ this._that._map.removeSource(SOURCE_DEVICE_CRUISE_POINT);
+ }
+ }
+
+ _renderDevice() {
+ if (!this._that._points.length) {
+ const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
+ if (source) source.setData(turf.featureCollection([]));
+ return;
+ }
+
+ const point = this._that._getTimelyData(this._that.elapsedMs);
+ this._that.timelyData = point;
+ if (!point) return;
+
+ const { lng, lat, yaw } = point;
+ const pointFeature = turf.point([lng, lat], {
+ ...point,
+ yaw: (+Number(yaw) || 0).toFixed(2),
+ });
+
+ const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
+ source.setData(pointFeature);
+ }
+
+ _ticker = timestamp => {
+ this._that.elapsedMs += Math.round((timestamp - this._that._lastFrameAt) * this._that.speedRate);
+ this._that._lastFrameAt = timestamp;
+ if (this._that.elapsedMs > this._that.totalTime) {
+ return;
+ }
+
+ // 在期望的间隔内_renderDevice,而不是每个tick都_renderDevice(目的:降低render频率,提高显示性能)
+ const now = Date.now();
+ const timeDiff = now - this._that._lastFrameTimestamp;
+ if (timeDiff > this._that._fpsInterval) {
+ this._that._lastFrameTimestamp = now - (timeDiff % this._that._fpsInterval); // 矫正时间戳
+ this._that._renderDevice();
+ }
+
+ this._that._timer = requestAnimationFrame(this._that._ticker);
+ };
+
+ // 开始播放巡航动画、恢复播放巡航动画
+ handlePlay() {
+ this._that.isPlaying = true;
+ this._that.isPaused = false;
+ this._that.isStopped = false;
+ this._that._lastFrameAt = performance.now();
+ requestAnimationFrame(this._that._ticker);
+ }
+
+ // 暂停播放巡航动画
+ handlePause() {
+ this._that.isPlaying = false;
+ this._that.isPaused = true;
+ this._that.isStopped = false;
+ cancelAnimationFrame(this._that._timer);
+ }
+
+ // 停止播放巡航动画
+ handleStop() {
+ this._that.isPlaying = false;
+ this._that.isPaused = false;
+ this._that.isStopped = true;
+ this._that._lastFrameAt = 0;
+ this._that.elapsedMs = 0;
+ cancelAnimationFrame(this._that._timer);
+ this._that._renderDevice();
+ this._that.timelyData = {};
+ }
+
+ clear() {
+ if (!this._that._map) return;
+
+ this._that.handleStop();
+ this._that._clearDevice();
+ this._that._reset();
+ this._that._dataSource = {};
+ this._that.timelyData = {};
+ }
+
+ destroy() {
+ this._that.clear();
+ this._that._timer = null;
+ this._that._map = null;
+ this._that.initStyle();
+ }
+}
+
+export default new DeviceCruise();
diff --git a/src/core/obstacleHelper/ObstacleHelper.js b/src/core/obstacleHelper/ObstacleHelper.js
new file mode 100644
index 0000000..97ad1c4
--- /dev/null
+++ b/src/core/obstacleHelper/ObstacleHelper.js
@@ -0,0 +1,17 @@
+/**
+ * 障碍物助手
+ */
+import obstacleRenderer from './obstacleRenderer';
+import blockObstacleRenderer from './blockObstacleRenderer';
+
+class ObstacleHelper {
+ static obstacleRenderer = obstacleRenderer;
+
+ static blockObstacleRenderer = blockObstacleRenderer;
+
+ static setMap(map) {
+ obstacleRenderer.setMap(map);
+ }
+}
+
+export default ObstacleHelper;
diff --git a/src/core/obstacleHelper/blockObstacleRenderer.js b/src/core/obstacleHelper/blockObstacleRenderer.js
new file mode 100644
index 0000000..d437f26
--- /dev/null
+++ b/src/core/obstacleHelper/blockObstacleRenderer.js
@@ -0,0 +1,31 @@
+/**
+ * 地块障碍物
+ */
+import ShapeRenderer from '../ShapeRenderer';
+
+class BlockObstacleRenderer extends ShapeRenderer {
+ _defaultOptions = {
+ showFill: true,
+ showStroke: false,
+ showLabel: true,
+ showLabelOnOver: true,
+ };
+
+ _defaultStyle = {
+ fillColor: 'rgba(255, 0, 0, 0.2)',
+ fillHoverColor: 'rgba(255, 0, 0, 0.8)',
+ strokeColor: 'rgba(255, 0, 0, 0.5)',
+ strokeHoverColor: 'rgba(255, 0, 0, 1)',
+ labelColor: 'rgba(0, 0, 0, 1)',
+ labelStrokeColor: 'rgba(255, 255, 255, 1)',
+ labelMinZoom: 3,
+ };
+
+ constructor() {
+ super('block-obstacle');
+ this.updateOptions();
+ this.updateStyle();
+ }
+}
+
+export default new BlockObstacleRenderer();
diff --git a/src/core/obstacleHelper/obstacleRenderer.js b/src/core/obstacleHelper/obstacleRenderer.js
new file mode 100644
index 0000000..860fda8
--- /dev/null
+++ b/src/core/obstacleHelper/obstacleRenderer.js
@@ -0,0 +1,31 @@
+/**
+ * 所有障碍物
+ */
+import ShapeRenderer from '../ShapeRenderer';
+
+class ObstacleRenderer extends ShapeRenderer {
+ _defaultOptions = {
+ showFill: true,
+ showStroke: false,
+ showLabel: true,
+ showLabelOnOver: true,
+ };
+
+ _defaultStyle = {
+ fillColor: 'rgba(255, 0, 0, 0.2)',
+ fillHoverColor: 'rgba(255, 0, 0, 0.8)',
+ strokeColor: 'rgba(255, 0, 0, 0.5)',
+ strokeHoverColor: 'rgba(255, 0, 0, 1)',
+ labelColor: 'rgba(0, 0, 0, 1)',
+ labelStrokeColor: 'rgba(255, 255, 255, 1)',
+ labelMinZoom: 3,
+ };
+
+ constructor() {
+ super('obstacle');
+ this.updateOptions();
+ this.updateStyle();
+ }
+}
+
+export default new ObstacleRenderer();
diff --git a/src/core/trackRenderer.js b/src/core/trackRenderer.js
new file mode 100644
index 0000000..49ec5d8
--- /dev/null
+++ b/src/core/trackRenderer.js
@@ -0,0 +1,443 @@
+/**
+ * 多轨迹在地图上显示
+ */
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import EventDispatcher from './EventDispatcher';
+
+const SOURCE_RUNNING_TRACK_LINE = 'running-track-line-source';
+const LAYER_RUNNING_TRACK_LINE = 'running-track-line-layer';
+const SOURCE_WORKING_TRACK_LINE = 'running-working-line-source';
+const LAYER_WORKING_TRACK_LINE = 'running-working-line-layer';
+const SOURCE_START_TRACK_POINT = 'start-track-point-source';
+const LAYER_START_TRACK_POINT = 'start-track-point-layer';
+const SOURCE_END_TRACK_POINT = 'end-track-point-source';
+const LAYER_END_TRACK_POINT = 'end-track-point-layer';
+
+class TrackRenderer extends EventDispatcher {
+ // 地图实例
+ _map = null;
+
+ // 轨迹容器
+ _tracksStore = [];
+
+ // 每条轨迹唯一键
+ _trackKey = 'id';
+
+ // 鼠标经过的轨迹id
+ _hoveredId = null;
+
+ // 鼠标经过显示的气泡框
+ _popup = new mapbox.Popup({ closeButton: false });
+
+ // 以回调形式获取popup显示内容
+ _getPopupHtml = () => 'Nothing';
+
+ // 显示配置项
+ _defaultOptions = {
+ // 最小显示级别
+ minZoom: 3,
+ // 显示端点
+ showEndpoint: true,
+ // 显示作业轨迹
+ showWorkingTrack: true,
+ // 显示行驶轨迹
+ showRunningTrack: true,
+ // 显示气泡框
+ showPopup: false,
+ };
+
+ _options = {};
+
+ // 视觉样式
+ _defaultStyle = {
+ startPointColor: 'rgba(0,0,255,0.8)',
+ endPointColor: 'rgba(0,255,0,0.8)',
+ pointStrokeColor: '#ffffff',
+ runningTrackColor: 'rgba(255,255,255,0.3)',
+ runningTrackHoverColor: 'rgba(255,255,255,0.8)',
+ workingTrackColor: 'rgba(255,0,0,0.3)',
+ workingTrackHoverColor: 'rgba(255,0,0,0.6)',
+ };
+
+ _style = {};
+
+ // 所有轨迹起始点
+ get _startPointsFeature() {
+ if (!this._options.showEndpoint) return turf.featureCollection([]);
+
+ const positions = this._tracksStore.map(({ points }) => {
+ const [{ lng, lat }] = points;
+ return [lng, lat];
+ });
+ return turf.multiPoint(positions);
+ }
+
+ // 所有轨迹结束点
+ get _endPointsFeature() {
+ if (!this._options.showEndpoint) return turf.featureCollection([]);
+
+ const positions = this._tracksStore.map(({ points }) => {
+ const { lng, lat } = points[points.length - 1];
+ return [lng, lat];
+ });
+ return turf.multiPoint(positions);
+ }
+
+ // 行驶轨迹集合
+ get _runningTrackFeatureCollection() {
+ if (!this._options.showRunningTrack) return turf.featureCollection([]);
+
+ const features = this._tracksStore.map(detail => {
+ const { points, [this._trackKey]: id, ...others } = detail;
+ const positions = points.map(({ lng, lat }) => ([lng, lat]));
+ return turf.lineString(positions, { id, ...others }, { id });
+ });
+ return turf.featureCollection(features);
+ }
+
+ // 作业轨迹集合
+ get _workingTrackFeatureCollection() {
+ if (!this._options.showWorkingTrack) return turf.featureCollection([]);
+
+ const features = this._tracksStore.map(({ points, [this._trackKey]: id }) => {
+ const segments = TrackRenderer.pickWorkingTrackSegments(points);
+ return turf.multiLineString(segments, {}, { id });
+ });
+ return turf.featureCollection(features);
+ }
+
+ // 限位框
+ get _boundingBox() {
+ return turf.bbox(this._runningTrackFeatureCollection);
+ }
+
+ // 拾取某个轨迹里的各个作业片段
+ static pickWorkingTrackSegments(trackPoints) {
+ const result = [];
+ let segment = [];
+ trackPoints.forEach(({ lng, lat, flowSpeed }) => {
+ if (flowSpeed > 0) {
+ segment.push([lng, lat]);
+ } else {
+ if (segment.length >= 2) {
+ result.push(segment);
+ }
+ segment = [];
+ }
+ });
+ return result;
+ }
+
+ constructor() {
+ super(['rendered']);
+
+ this.initOptions();
+ this.initStyle();
+
+ makeObservable(this, {
+ _tracksStore: observable,
+ _startPointsFeature: computed,
+ _endPointsFeature: computed,
+ _runningTrackFeatureCollection: computed,
+ _workingTrackFeatureCollection: computed,
+ loadTracks: action,
+ destroy: action,
+ });
+
+ reaction(() => this._tracksStore, () => {
+ if (this._tracksStore.length) {
+ this._render();
+ } else {
+ this._clear();
+ }
+ });
+ }
+
+ // 初始化配置(需在loadTracks之前配置)
+ initOptions(options = {}) {
+ this._options = {
+ ...this._defaultOptions,
+ ...options,
+ };
+ }
+
+ // 初始化视觉演示(需在loadTracks之前配置)
+ initStyle(style = {}) {
+ this._style = {
+ ...this._defaultStyle,
+ ...style,
+ };
+ }
+
+ // 设置地图实例
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ // 载入轨迹数据
+ loadTracks(list, trackKey = 'id') {
+ if (!this._map) {
+ throw new Error('请先设置地图实例');
+ }
+ // list = [{ id: 123, points: [{ lng, lat, yaw, flowSpeed }] }];
+ this._tracksStore = (list || []).slice();
+ this._trackKey = trackKey;
+ }
+
+ // 设置气泡框显示内容回调
+ setPopupContentCallback(callback) {
+ if (typeof callback !== 'function') {
+ throw new Error('入参必须是一个函数');
+ }
+ this._getPopupHtml = callback;
+ }
+
+ _render() {
+ if (this._options.showRunningTrack) this._renderRunningTrack();
+ if (this._options.showWorkingTrack) this._renderWorkingTrack();
+ if (this._options.showRunningTrack) {
+ this._renderStartPoint();
+ this._renderEndPoint();
+ }
+ this._trigger('rendered');
+ }
+
+ _renderStartPoint() {
+ const source = this._map.getSource(SOURCE_START_TRACK_POINT);
+ if (!source) {
+ this._map.addSource(SOURCE_START_TRACK_POINT, {
+ type: 'geojson',
+ data: this._startPointsFeature,
+ });
+
+ this._map.addLayer({
+ id: LAYER_START_TRACK_POINT,
+ type: 'circle',
+ source: SOURCE_START_TRACK_POINT,
+ paint: {
+ 'circle-radius': 4,
+ 'circle-color': this._style.startPointColor,
+ 'circle-stroke-width': 1,
+ 'circle-stroke-color': this._style.pointStrokeColor,
+ 'circle-stroke-opacity': 0.6,
+ 'circle-opacity': [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._options.minZoom - 0.01, 0,
+ this._options.minZoom, 1,
+ ],
+ },
+ });
+ } else {
+ source.setData(this._startPointsFeature);
+ }
+ }
+
+ _renderEndPoint() {
+ const source = this._map.getSource(SOURCE_END_TRACK_POINT);
+ if (!source) {
+ this._map.addSource(SOURCE_END_TRACK_POINT, {
+ type: 'geojson',
+ data: this._endPointsFeature,
+ });
+
+ this._map.addLayer({
+ id: LAYER_END_TRACK_POINT,
+ type: 'circle',
+ source: SOURCE_END_TRACK_POINT,
+ paint: {
+ 'circle-radius': 4,
+ 'circle-color': this._style.endPointColor,
+ 'circle-stroke-width': 1,
+ 'circle-stroke-color': this._style.pointStrokeColor,
+ 'circle-stroke-opacity': 0.6,
+ 'circle-opacity': [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._options.minZoom - 0.01, 0,
+ this._options.minZoom, 1,
+ ],
+ },
+ });
+ } else {
+ source.setData(this._endPointsFeature);
+ }
+ }
+
+ _renderRunningTrack() {
+ const source = this._map.getSource(SOURCE_RUNNING_TRACK_LINE);
+ if (!source) {
+ this._map.addSource(SOURCE_RUNNING_TRACK_LINE, {
+ type: 'geojson',
+ data: this._runningTrackFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: LAYER_RUNNING_TRACK_LINE,
+ type: 'line',
+ source: SOURCE_RUNNING_TRACK_LINE,
+ paint: {
+ 'line-color': [
+ 'case',
+ ['boolean', ['feature-state', 'hover'], false],
+ this._style.runningTrackHoverColor,
+ this._style.runningTrackColor,
+ ],
+ 'line-width': 3,
+ 'line-opacity': [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._options.minZoom - 0.01, 0,
+ this._options.minZoom, 1,
+ ],
+ },
+ layout: {
+ 'line-cap': 'round',
+ 'line-join': 'round',
+ },
+ });
+
+ this._map.on('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove);
+ this._map.on('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave);
+ this._map.on('click', LAYER_RUNNING_TRACK_LINE, this._onClick);
+ } else {
+ source.setData(this._runningTrackFeatureCollection);
+ }
+ }
+
+ _renderWorkingTrack() {
+ const source = this._map.getSource(SOURCE_WORKING_TRACK_LINE);
+ if (!source) {
+ this._map.addSource(SOURCE_WORKING_TRACK_LINE, {
+ type: 'geojson',
+ data: this._workingTrackFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: LAYER_WORKING_TRACK_LINE,
+ type: 'line',
+ source: SOURCE_WORKING_TRACK_LINE,
+ paint: {
+ 'line-color': [
+ 'case',
+ ['boolean', ['feature-state', 'hover'], false],
+ this._style.workingTrackHoverColor,
+ this._style.workingTrackColor,
+ ],
+ 'line-width': 6,
+ 'line-opacity': [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._options.minZoom - 0.01, 0,
+ this._options.minZoom, 1,
+ ],
+ },
+ layout: {
+ 'line-cap': 'round',
+ 'line-join': 'round',
+ },
+ });
+ } else {
+ source.setData(this._workingTrackFeatureCollection);
+ }
+ }
+
+ _onMouseMove = e => {
+ if (this._map.getZoom() < this._options.minZoom) return;
+ this._map.getCanvas().style.cursor = 'pointer';
+ if (e.features.length > 0) {
+ if (this._hoveredId !== null) {
+ const id = this._hoveredId;
+ if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: false });
+ if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: false });
+ }
+ const [{ id }] = e.features;
+ if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: true });
+ if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: true });
+ this._hoveredId = id;
+ }
+ if (this._options.showPopup) {
+ const [{ properties: detail }] = e.features;
+ this._popup.setLngLat(e.lngLat).setHTML(this._getPopupHtml(detail)).addTo(this._map);
+ }
+ };
+
+ _onMouseLeave = () => {
+ if (this._map.getZoom() < this._options.minZoom) return;
+ this._map.getCanvas().style.cursor = '';
+ if (this._hoveredId !== null) {
+ if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id: this._hoveredId }, { hover: false });
+ if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id: this._hoveredId }, { hover: false });
+ }
+ this._hoveredId = null;
+ if (this._options.showPopup) {
+ this._popup.remove();
+ }
+ };
+
+ _onClick = e => {
+ if (this._map.getZoom() < this._options.minZoom) return;
+ if (!e.features.length) return;
+ const [{ properties: detail }] = e.features;
+ const { points } = this._tracksStore.find(({ [this._trackKey]: key }) => key === detail.id) || {};
+ this._trigger('click', { ...detail, points: (points || []).map(item => ({ ...item })) });
+ };
+
+ // 缩放到所有轨迹总边界
+ fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
+ if (!this._tracksStore.length) return;
+ this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
+ this._map.fitBounds(this._boundingBox, {
+ duration: 2000,
+ padding: {
+ top: top + cut,
+ bottom: bottom + cut,
+ left: left + cut,
+ right: right + cut,
+ },
+ });
+ }
+
+ _clear() {
+ if (!this._map) return;
+ if (this._map.getSource(SOURCE_RUNNING_TRACK_LINE)) {
+ this._map.off('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove);
+ this._map.off('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave);
+ this._map.off('click', LAYER_RUNNING_TRACK_LINE, this._onClick);
+ this._map.removeLayer(LAYER_RUNNING_TRACK_LINE);
+ this._map.removeSource(SOURCE_RUNNING_TRACK_LINE);
+ }
+ if (this._map.getSource(SOURCE_WORKING_TRACK_LINE)) {
+ this._map.removeLayer(LAYER_WORKING_TRACK_LINE);
+ this._map.removeSource(SOURCE_WORKING_TRACK_LINE);
+ }
+ if (this._map.getSource(SOURCE_START_TRACK_POINT)) {
+ this._map.removeLayer(LAYER_START_TRACK_POINT);
+ this._map.removeSource(SOURCE_START_TRACK_POINT);
+ }
+ if (this._map.getSource(SOURCE_END_TRACK_POINT)) {
+ this._map.removeLayer(LAYER_END_TRACK_POINT);
+ this._map.removeSource(SOURCE_END_TRACK_POINT);
+ }
+ if (this._options.showPopup) {
+ this._popup.remove();
+ }
+ }
+
+ destroy() {
+ this._clear();
+ this._map = null;
+ this._tracksStore = [];
+ this._getPopupHtml = () => 'Nothing';
+ this.initOptions();
+ this.initStyle();
+ }
+}
+
+export default new TrackRenderer();
diff --git a/src/layout/MainContainer.vue b/src/layout/MainContainer.vue
index e29dbc1..a872f30 100644
--- a/src/layout/MainContainer.vue
+++ b/src/layout/MainContainer.vue
@@ -3,10 +3,11 @@
import MapLayer from '@/layout/MapLayer.vue';
import SideMenu from '@/layout/components/SideMenu.vue';
import TopBar from '@/layout/components/TopBar.vue';
+ import LiveDialog from '@/layout/components/LiveDialog.vue';
+ import PasswordEditor from '@/layout/components/PasswordEditor.vue';
-
@@ -16,10 +17,14 @@
+
+
+
+
diff --git a/src/layout/components/LiveDialog.vue b/src/layout/components/LiveDialog.vue
new file mode 100644
index 0000000..eb55846
--- /dev/null
+++ b/src/layout/components/LiveDialog.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ 直播
+
+
+
+
+
+
+
diff --git a/src/layout/components/MultifunctionalBar.vue b/src/layout/components/MultifunctionalBar.vue
index 478b10e..bbbe995 100644
--- a/src/layout/components/MultifunctionalBar.vue
+++ b/src/layout/components/MultifunctionalBar.vue
@@ -2,83 +2,76 @@
import { onMounted, ref, watchEffect } from 'vue';
import commonRefs from '@/utils/commonRefs';
import { useGlobalSettings } from '@/views/common/useGlobalSettings';
+ import mapConfig from '@/config/map';
+ import eventBus from '@/utils/eventBus';
// import { useGlobalFarm } from '@/views/common/useGlobalFarm';
// import { useGlobalFields } from '@/views/common/useGlobalFields';
- import eventBus from '@/utils/eventBus';
+ // import eventBus from '@/utils/eventBus';
const isReady = ref(false);
- const showMapHd = ref(true);
- const showFieldFill = ref(true);
- const showFieldName = ref(true);
+ const showNoFlyZone = ref(true);
+ // const showFieldFill = ref(true);
+ // const showFieldName = ref(true);
watchEffect(() => {
const settings = useGlobalSettings().valueOf();
- showMapHd.value = settings.showMapHd;
- showFieldFill.value = settings.showFieldFill;
- showFieldName.value = settings.showFieldName;
+ showNoFlyZone.value = settings.showNoFlyZone;
+ // showFieldFill.value = settings.showFieldFill;
+ // showFieldName.value = settings.showFieldName;
});
- function init() {
- eventBus.emit('show-map-hd-layer', showMapHd.value);
- }
+ // function init() {
+ // eventBus.emit('show-map-hd-layer', showMapHd.value);
+ // }
- function onToggleMapHd() {
- const result = !showMapHd.value;
- eventBus.emit('show-map-hd-layer', result);
- useGlobalSettings().set('showMapHd', result);
- }
+ // function onToggleNoFlyZone() {
+ // // const result = !showMapHd.value;
+ // // eventBus.emit('show-map-hd-layer', result);
+ // // useGlobalSettings().set('showMapHd', result);
+ // }
- function onToggleFill(val) {
+ function onToggleNoFlyZone(val) {
// useGlobalFields().toggleFill(val);
- useGlobalSettings().set('showFieldFill', val);
+ useGlobalSettings().set('showNoFlyZone', val);
}
- function onToggleName(val) {
- // useGlobalFields().toggleName(val);
- useGlobalSettings().set('showFieldName', val);
- }
+ // function onToggleName(val) {
+ // // useGlobalFields().toggleName(val);
+ // useGlobalSettings().set('showFieldName', val);
+ // }
+
+ let map;
onMounted(async () => {
- await commonRefs.getRef('map');
+ map = await commonRefs.getRef('map');
isReady.value = true;
- init();
+ // init();
});
+
+ function onResetZoom() {
+ map.setZoom(mapConfig.zoom);
+ map.setCenter(mapConfig.center);
+ // map.setPitch(0);
+ // map.setBearing(0);
+ eventBus.emit('hide-all-panels');
+ }
-
-
+
+
-
-
-
-
-
-
-
-
-
-
- 高清图层
-
-
-
-
-
+
-
- 地块形状
-
-
-
- 地块名称
+
+ 禁飞区
@@ -138,6 +131,9 @@
}
.root {
+ position: absolute;
+ bottom: 40px;
+ right: 10px;
background-color: fade(black, 35%);
backdrop-filter: blur(6px);
border-radius: 100px;
diff --git a/src/layout/components/PasswordEditor.vue b/src/layout/components/PasswordEditor.vue
new file mode 100644
index 0000000..d203307
--- /dev/null
+++ b/src/layout/components/PasswordEditor.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+ 更改密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 取消
+
+
+
+
+
+
+
diff --git a/src/layout/components/SideMenu.vue b/src/layout/components/SideMenu.vue
index 68a27e6..4c8477b 100644
--- a/src/layout/components/SideMenu.vue
+++ b/src/layout/components/SideMenu.vue
@@ -1,17 +1,30 @@
-
+
-
-
-
+
+
+
+
+
+
-
+
-
+
案例模板
-
+
-
+
+
- 实时监控
+ 实时监控
-
+
-
+
+
- 设备管理
+ 设备管理
-
+
-
+
+
- 制造商管理
+ 架次管理
-
+
-
+
+
- 监管者管理
+ 禁飞区管理
-
+
-
+
+
- 架次管理
+ 成果管理
-
+
-
+
+
- 禁飞区管理
+ 制造商管理
-
+
-
+
+
- 数据分析
+ 监管者管理
+
+
+
+
+
+
+ 数据分析
diff --git a/src/layout/components/TopBar.vue b/src/layout/components/TopBar.vue
index abbe05b..b626cd3 100644
--- a/src/layout/components/TopBar.vue
+++ b/src/layout/components/TopBar.vue
@@ -1,9 +1,33 @@
-
-
- 云端无人机管理系统(管理员)
-
-
-
- 监察者模式
-
-
-
-
+
+
+
+
+
+
+
+ {{ userInfo.companyName || userInfo.username }}
+
+
+
+
+
+
+ 观看
-
-
-
- 退出登录
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 更改密码
+
+
+ 退出登录
+
+
+
+
+
diff --git a/src/views/AchievementView/components/ImageBar.vue b/src/views/AchievementView/components/ImageBar.vue
new file mode 100644
index 0000000..0d832f4
--- /dev/null
+++ b/src/views/AchievementView/components/ImageBar.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/AchievementView/components/MediaManage.vue b/src/views/AchievementView/components/MediaManage.vue
new file mode 100644
index 0000000..cb9090c
--- /dev/null
+++ b/src/views/AchievementView/components/MediaManage.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+ 媒体管理
+
+
+
+
+
+
+
diff --git a/src/views/AchievementView/components/SortieDetail.vue b/src/views/AchievementView/components/SortieDetail.vue
new file mode 100644
index 0000000..56db09c
--- /dev/null
+++ b/src/views/AchievementView/components/SortieDetail.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+ 查看媒体
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/DeviceView/DeviceView.vue b/src/views/DeviceView/DeviceView.vue
new file mode 100644
index 0000000..42e4d1f
--- /dev/null
+++ b/src/views/DeviceView/DeviceView.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 录入设备
+
+
+
+
+
+ 关键字查询
+
+ 查询
+ 重置
+
+
+
+
+
+ 在线
+ 离线
+
+
+
+ 未锁定
+ 已锁定
+
+
+
+
+ {{ !row.locked ? '锁定' : '解锁' }}
+
+
+ 删除
+
+ 编辑
+
+
+
+
+
+
+
+
+
diff --git a/src/views/DeviceView/components/DeviceEditor.vue b/src/views/DeviceView/components/DeviceEditor.vue
new file mode 100644
index 0000000..2dc5ba5
--- /dev/null
+++ b/src/views/DeviceView/components/DeviceEditor.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+ {{ formData.id ? '更新' : '录入' }}设备
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 取消
+
+
+
+
+
+
+
diff --git a/src/views/ExampleView/ExampleView.vue b/src/views/ExampleView/ExampleView.vue
index b43b08b..73fab11 100644
--- a/src/views/ExampleView/ExampleView.vue
+++ b/src/views/ExampleView/ExampleView.vue
@@ -1,8 +1,88 @@
- ExampleView
+
+
+
+
+
+
+
diff --git a/src/views/MonitorView/components/DeviceStatBox.vue b/src/views/MonitorView/components/DeviceStatBox.vue
new file mode 100644
index 0000000..1c74dcb
--- /dev/null
+++ b/src/views/MonitorView/components/DeviceStatBox.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/MonitorView/components/DroneDetail.vue b/src/views/MonitorView/components/DroneDetail.vue
new file mode 100644
index 0000000..cea5151
--- /dev/null
+++ b/src/views/MonitorView/components/DroneDetail.vue
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 农机状态 |
+ {{ dynamicInfo.landStatus ? '-' : '作业中' }} |
+
+
+ 当前位置 |
+ |
+
+
+
+
+
+
+ 定位精度 |
+ {{ }} |
+
+
+ 经纬度 |
+
+
+
+
+
+
+
+ 对地高度 |
+ {{ dynamicInfo.height }}m |
+
+
+ 水平速度 |
+ {{ dynamicInfo.xspeed }}m/s |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 喷洒流速 |
+ {{ dynamicInfo.flowSpeed }}L/min |
+
+
+
+
+
+
+
+
+
+
+
+ 农机ID |
+ {{ baseInfo.droneId }} |
+
+
+ 农机类型 |
+ {{ baseInfo.deviceType }} |
+
+
+ 制造商 |
+ {{ baseInfo.zzAccountName }} |
+
+
+ 制造商 |
+ {{ baseInfo.zzAccountName }} |
+
+
+ 制造商 |
+ {{ baseInfo.zzAccountName }} |
+
+
+ 机型 |
+ {{ baseInfo.modelName }} |
+
+
+ 运营人 |
+ {{ baseInfo.owner }} |
+
+
+ 实名登记标识 |
+ {{ baseInfo.regMark }} |
+
+
+
+
+
+
+
+
+
+ 总作业时长 |
+ {{ dynamicInfo.flyTotalDuration }}h |
+
+
+ 总作业里程 |
+ {{ dynamicInfo.flyTotalMileage }}km |
+
+
+ 总作业亩数 |
+ 10000亩 |
+
+
+ 飞控总亩数 |
+ 10000亩 |
+
+
+ 总喷洒量 |
+ {{ dynamicInfo.sparyTotalAmount }}L |
+
+
+ 总作业架次 |
+ {{ dynamicInfo.flySeqCount }} |
+
+
+
+
+
+
+
+
diff --git a/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue b/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
index 764dde9..356361e 100644
--- a/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
+++ b/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
@@ -173,6 +173,7 @@
noFlyZoneRenderer.fitShape(formData.value.id);
noFlyZoneRenderer.clear();
noFlyZoneEditor.loadDataSource({ id: formData.value.id, points: formData.value.orbit });
+ showOperationTips(2);
} else {
noFlyZoneCreator.start();
showOperationTips(1);
diff --git a/src/views/NoFlyZoneView/NoFlyZoneView.vue b/src/views/NoFlyZoneView/NoFlyZoneView.vue
index 6c8c894..1de2b0f 100644
--- a/src/views/NoFlyZoneView/NoFlyZoneView.vue
+++ b/src/views/NoFlyZoneView/NoFlyZoneView.vue
@@ -70,7 +70,7 @@
});
}
- loadList();
+ loadList({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined });
function onPageChange({ current, pageSize }) {
loadList({ page: current, pageSize });
@@ -85,6 +85,11 @@
loadList({ search: search.value });
}
+ function onResetList() {
+ search.value = undefined;
+ onSearchList();
+ }
+
const columns = [
{ colKey: 'detailAddress', title: '禁飞区名称', ellipsis: true, width: '300px' },
{
@@ -164,13 +169,19 @@
}
onUnmounted(() => {
- noFlyZoneRenderer.clear();
+ // noFlyZoneRenderer.clear();
+ noFlyZoneRenderer.destroy();
});
-
- 禁飞区管理
+
+
+
+
+
+
+
新增禁飞区
@@ -183,10 +194,12 @@
关键字查询
查询
+ 重置
- 监管者管理
+
+
+
+
+
+
新增监管者
@@ -59,6 +65,7 @@
+
+
+
+
diff --git a/src/views/SortieView/SortieView.vue b/src/views/SortieView/SortieView.vue
new file mode 100644
index 0000000..52163db
--- /dev/null
+++ b/src/views/SortieView/SortieView.vue
@@ -0,0 +1,9731 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 展开列表
+
+
+
+
+
+ 飞控ID
+
+ 查询
+ 重置
+
+
+
+
+
+
+ 媒体管理
+ 查看媒体
+ 轨迹回放
+
+
+
+
+
+
+
+
+
+ 管理媒体
+ 查看媒体
+ 轨迹回放
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/SortieView/components/ImageBar.vue b/src/views/SortieView/components/ImageBar.vue
new file mode 100644
index 0000000..9705965
--- /dev/null
+++ b/src/views/SortieView/components/ImageBar.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/SortieView/components/MediaManage.vue b/src/views/SortieView/components/MediaManage.vue
new file mode 100644
index 0000000..cb9090c
--- /dev/null
+++ b/src/views/SortieView/components/MediaManage.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+ 媒体管理
+
+
+
+
+
+
+
diff --git a/src/views/SortieView/components/SortieDetail.vue b/src/views/SortieView/components/SortieDetail.vue
new file mode 100644
index 0000000..9c030dd
--- /dev/null
+++ b/src/views/SortieView/components/SortieDetail.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+ 查看媒体
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/common/useGlobalSettings.js b/src/views/common/useGlobalSettings.js
index 3e31754..e6ffb9f 100644
--- a/src/views/common/useGlobalSettings.js
+++ b/src/views/common/useGlobalSettings.js
@@ -6,9 +6,9 @@ import { createGlobalState, useStorage } from '@vueuse/core';
export const useGlobalSettings = createGlobalState(() => {
const settings = useStorage('settings', {
- showMapHd: true,
- showFieldFill: true,
- showFieldName: true,
+ showNoFlyZone: true,
+ // showFieldFill: true,
+ // showFieldName: true,
});
const valueOf = () => toValue(settings);
diff --git a/vite.config.js b/vite.config.js
index d20cd03..f5e09b4 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -62,17 +62,18 @@ export default defineConfig(({ mode }) => {
// esbuild: { loader: { '.js': '.jsx' } },
server: {
- // host: true, // 监听所有地址
+ host: true, // 监听所有地址
proxy: {
// 使用 proxy 实例
- // '/api': {
- // // target: 'http://192.168.10.79:9999',
- // // target: 'http://192.168.10.111:9900',
- // // target: 'http://192.168.10.32:8102',
- // // target: 'http://192.168.10.32:9999',
- // changeOrigin: true,
- // rewrite: (path) => path.replace(/^\/api/, ''),
- // },
+ '/api': {
+ // target: 'http://192.168.10.79:9998',
+ target: 'http://sgcloud-test.jiagutech.com/api',
+ // target: 'http://192.168.10.111:9900',
+ // target: 'http://192.168.10.32:8102',
+ // target: 'http://192.168.10.32:9999',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ },
// 案例
// // 字符串简写写法:http://localhost:5173/foo -> http://localhost:4567/foo
// '/foo': 'http://localhost:4567',