DrawTools.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <div class="draw-tools-container">
  3. <div class="draw-item" @mouseover="showColorSelect = true" @mouseleave="showColorSelect = false">
  4. <div class="item-label">画笔选择</div>
  5. <i class="color-box" :style="{ backgroundColor: colorList[drawerColorIndex]?.value }" />
  6. <!--弹窗选择器-->
  7. <div v-show="showColorSelect" ref="selectBoxRef" class="select-box" style="top: -123px; padding-bottom: 20px">
  8. <div class="box-content">
  9. <div
  10. v-for="(item, index) in colorList"
  11. :key="index"
  12. :class="mouseToolState.drawType === item.value ? 'select-item active' : 'select-item'"
  13. @click="selectColor(index)"
  14. >
  15. <i class="color-box" :style="{ backgroundColor: item.value }" />
  16. <span>{{ item.label }}</span>
  17. </div>
  18. </div>
  19. </div>
  20. </div>
  21. <div class="draw-item" @mouseover="showDrawTypeSelect = true" @mouseleave="showDrawTypeSelect = false">
  22. <div class="item-label">框面类型</div>
  23. <!--弹窗选择器-->
  24. <div v-show="showDrawTypeSelect" ref="selectBoxRef2" class="select-box" style="top: -64px; padding-bottom: 20px">
  25. <div class="box-content">
  26. <div
  27. v-for="(item, index) in drawTypeList"
  28. :key="index"
  29. :class="mouseToolState.drawType === item.value ? 'select-item active' : 'select-item'"
  30. @click="selectDrawType(index)"
  31. >
  32. <span>{{ item.label }}</span>
  33. </div>
  34. </div>
  35. </div>
  36. </div>
  37. <div class="draw-item" @mouseover="showGraphicsTypeSelect = true" @mouseleave="showGraphicsTypeSelect = false">
  38. <div class="item-label">图像类型</div>
  39. <!--弹窗选择器-151-->
  40. <div v-show="showGraphicsTypeSelect" ref="selectBoxRef2" class="select-box" style="top: -91px; padding-bottom: 20px">
  41. <div class="box-content">
  42. <div
  43. v-for="(item, index) in graphicsTypeList"
  44. :key="index"
  45. :class="mouseToolState.graphicsType === item.value ? 'select-item active' : 'select-item'"
  46. @click="selectGraphicsType(index)"
  47. >
  48. <span>{{ item.label }}</span>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="draw-item" @click="openDraw">{{ mouseToolState.drawing ? '关闭绘制' : '开启绘制' }}</div>
  54. <div class="draw-item" @click="handleUndo">撤销</div>
  55. </div>
  56. </template>
  57. <script lang="ts" setup name="DrawTools">
  58. import { useHistory } from '@/hooks/useHistory';
  59. import { nanoid } from 'nanoid';
  60. import { deepClone } from '@/utils';
  61. import * as turf from '@turf/turf';
  62. import Overlay from 'ol/Overlay';
  63. interface ListItem {
  64. label: string;
  65. value: string;
  66. }
  67. const props = defineProps({
  68. activeMap: String
  69. });
  70. const AMapType = ['vectorgraph', 'satellite'];
  71. const getDrawTool = inject('getDrawTool');
  72. const getMap = inject('getMap');
  73. const getMapUtils = inject('getMapUtils');
  74. const emits = defineEmits(['handleAnalysisData']);
  75. const { currentState, commit, undo, history, future } = useHistory();
  76. const mouseToolState = reactive({
  77. drawing: false,
  78. color: '#f80102',
  79. // 1线框 2区域
  80. drawType: '1',
  81. // 图形形状 circle圆形 rectangle矩形 polygon多边形
  82. graphicsType: 'circle'
  83. });
  84. let popup;
  85. let map = computed(() => {
  86. return getMap();
  87. });
  88. let mapUtils = computed(() => {
  89. return getMapUtils();
  90. });
  91. watch(
  92. mouseToolState,
  93. () => {
  94. const drawTool = getDrawTool();
  95. if (mouseToolState.drawing) {
  96. drawOptions.color = mouseToolState.color;
  97. drawOptions.drawType = mouseToolState.drawType;
  98. drawOptions.graphicsType = mouseToolState.graphicsType;
  99. drawTool.drawGraphics2(drawOptions);
  100. if (AMapType.includes(props.activeMap)) {
  101. drawTool.getMouseTool().on('draw', onDraw);
  102. } else {
  103. drawTool.getMouseTool().on('drawend', onDraw2);
  104. map.value.un('click', onMapClick);
  105. }
  106. } else {
  107. mouseToolState.drawing = false;
  108. drawTool.closeDraw();
  109. if (!AMapType.includes(props.activeMap)) {
  110. map.value.on('click', onMapClick);
  111. }
  112. }
  113. },
  114. {
  115. deep: true
  116. }
  117. );
  118. const drawOptions = reactive({
  119. color: '#f80102',
  120. // 1线框 2区域
  121. drawType: '1',
  122. // 图形形状 circle圆形 rectangle矩形 polygon多边形
  123. graphicsType: 'circle'
  124. });
  125. const selectBoxRef = ref(null);
  126. const selectBoxRef2 = ref(null);
  127. const selectBoxRef3 = ref(null);
  128. // 显示画笔选择弹窗
  129. let showColorSelect = ref(false);
  130. // 画笔颜色索引
  131. let drawerColorIndex = ref(0);
  132. // 画笔颜色列表
  133. const colorList: ListItem[] = reactive([
  134. { label: '红色', value: '#f80102' },
  135. { label: '橙色', value: '#f4511e' },
  136. { label: '蓝色', value: '#0197f8' },
  137. { label: '绿色', value: '#42b884' }
  138. ]);
  139. // 选中颜色
  140. const selectColor = (index) => {
  141. drawerColorIndex.value = index;
  142. mouseToolState.color = colorList[index].value;
  143. showColorSelect.value = false;
  144. };
  145. const overlays = [];
  146. const overlaysData = [];
  147. // 点击颜色选择之外
  148. onClickOutside(selectBoxRef, () => {
  149. if (!showColorSelect.value) return;
  150. showColorSelect.value = false;
  151. });
  152. // 绘制类型列表
  153. const drawTypeList: ListItem[] = reactive([
  154. { label: '线框', value: '1' },
  155. { label: '面框', value: '2' }
  156. ]);
  157. // 显示隐藏绘制类型弹窗
  158. let showDrawTypeSelect = ref(false);
  159. // 选择
  160. const selectDrawType = (index) => {
  161. mouseToolState.drawType = drawTypeList[index].value;
  162. showDrawTypeSelect.value = false;
  163. };
  164. // 点击绘制类型选择之外
  165. onClickOutside(selectBoxRef2, () => {
  166. if (!showDrawTypeSelect.value) return;
  167. showDrawTypeSelect.value = false;
  168. });
  169. // 绘制图形列表
  170. const graphicsTypeList: ListItem[] = reactive([
  171. { label: '圆形', value: 'circle' },
  172. { label: '矩形', value: 'rectangle' },
  173. { label: '多边形', value: 'polygon' }
  174. // { label: '索套', value: 'freePolygon' }
  175. ]);
  176. // 显隐图像类型弹窗
  177. let showGraphicsTypeSelect = ref(false);
  178. // 选择
  179. const selectGraphicsType = (index) => {
  180. mouseToolState.graphicsType = graphicsTypeList[index].value;
  181. showGraphicsTypeSelect.value = false;
  182. };
  183. // 点击绘制类型选择之外
  184. onClickOutside(selectBoxRef3, () => {
  185. if (!showGraphicsTypeSelect.value) return;
  186. showGraphicsTypeSelect.value = false;
  187. });
  188. // 开启结束绘制
  189. const openDraw = () => {
  190. mouseToolState.drawing = !mouseToolState.drawing;
  191. };
  192. // 高德地图绘制结束
  193. const onDraw = (event) => {
  194. const obj = event.obj;
  195. const id = nanoid();
  196. obj._opts.extData = {
  197. id: id
  198. };
  199. const data: any = {
  200. id: id,
  201. type: drawOptions.graphicsType,
  202. color: obj._opts.strokeColor,
  203. drawType: obj._opts.fillOpacity === 0 ? '1' : '2'
  204. };
  205. if (data.type === 'circle') {
  206. data.center = [obj.getCenter().lng, obj.getCenter().lat];
  207. data.radius = obj.getRadius();
  208. }
  209. const path = obj.getPath();
  210. // 将AMap.LngLat对象数组转换为经纬度数组
  211. const pathArr = path.map((lngLat) => {
  212. // 返回经度和纬度的数组
  213. return [lngLat.lng, lngLat.lat];
  214. });
  215. data.path = pathArr;
  216. overlays.push(obj);
  217. overlaysData.push(data);
  218. commit(deepClone(overlaysData));
  219. // 右击进入编辑
  220. obj.on('rightclick', handleRightClick);
  221. analysisSpatial(data);
  222. // 点击空间分析
  223. obj.on('click', function () {
  224. // 没在编辑时
  225. if (!mouseToolState.drawing) {
  226. analysisSpatial(data);
  227. }
  228. });
  229. };
  230. // 粤政图绘制结束
  231. const onDraw2 = (event) => {
  232. const feature = event.feature;
  233. const geometry = feature.getGeometry();
  234. const geometryType = geometry.getType();
  235. const id = nanoid();
  236. feature.set('id', id);
  237. feature.set('dotType', 'analysisSpatial');
  238. const data: any = {
  239. id: id,
  240. type: drawOptions.graphicsType,
  241. color: drawOptions.color,
  242. drawType: drawOptions.drawType
  243. };
  244. feature.set('extraData', data);
  245. if (geometryType === 'Circle') {
  246. data.center = geometry.getCenter();
  247. data.radius = geometry.getRadius();
  248. const data2 = turf.sector(data.center, data.radius, 0, 360);
  249. const pathArr = data2.geometry.coordinates[0];
  250. data.path = pathArr;
  251. } else if (geometryType === 'Polygon') {
  252. const coordinates = geometry.getCoordinates();
  253. data.path = coordinates[0];
  254. }
  255. overlays.push(feature);
  256. overlaysData.push(data);
  257. commit(deepClone(overlaysData));
  258. // 右击进入编辑
  259. let map = getMap();
  260. map.on('contextmenu', handleRightClick2);
  261. analysisSpatial(data);
  262. };
  263. let rightClickObj;
  264. // 图形右击事件
  265. let initContextMenu = false;
  266. const handleRightClick = (event) => {
  267. rightClickObj = event.target;
  268. const contextMenu = getDrawTool().getContextMenu();
  269. if (!initContextMenu) {
  270. // 右键删除按钮
  271. contextMenu.addItem(
  272. '删除',
  273. function () {
  274. contextMenu.close();
  275. deleteGraphics();
  276. },
  277. 0
  278. );
  279. initContextMenu = true;
  280. }
  281. contextMenu.open(getMap(), event.lnglat);
  282. };
  283. const handleRightClick2 = (event) => {
  284. event.preventDefault(); // 阻止默认的右键菜单弹出
  285. map.value.un('click', onMapClick);
  286. if (popup) {
  287. map.value.removeOverlay(popup);
  288. popup = undefined;
  289. }
  290. rightClickObj = map.value.forEachFeatureAtPixel(event.pixel, (feature2) => {
  291. return feature2;
  292. });
  293. let dotType = rightClickObj.get('dotType');
  294. if (!!rightClickObj && dotType === 'analysisSpatial') {
  295. const dom = document.createElement('div');
  296. const dom2 = document.createElement('div');
  297. dom.id = 'analysisSpatial-popup';
  298. dom2.className = 'menu-item';
  299. dom2.innerHTML = '删除';
  300. dom2.onclick = function () {
  301. map.value.on('click', onMapClick);
  302. deleteGraphics();
  303. };
  304. dom.appendChild(dom2);
  305. popup = new Overlay({
  306. element: dom,
  307. autoPan: true,
  308. positioning: 'bottom-right',
  309. stopEvent: false,
  310. offset: [0, 0]
  311. });
  312. const geometry = rightClickObj.getGeometry();
  313. const geometryType = geometry.getType();
  314. let coordinate = [];
  315. if (geometryType === 'Circle') {
  316. coordinate = geometry.getCenter();
  317. } else {
  318. const points = geometry.getCoordinates();
  319. const features = turf.points(points[0]);
  320. const center = turf.center(features);
  321. if (center && center.geometry && center.geometry.coordinates) {
  322. coordinate = center.geometry.coordinates;
  323. }
  324. }
  325. popup.setPosition(coordinate);
  326. map.value.addOverlay(popup);
  327. }
  328. };
  329. // 删除图形
  330. const deleteGraphics = () => {
  331. let id;
  332. if (AMapType.includes(props.activeMap)) {
  333. id = rightClickObj.getExtData()?.id;
  334. } else {
  335. unMapClick();
  336. id = rightClickObj.get('id');
  337. }
  338. if (!!id) {
  339. for (let i = 0; i < overlays.length; i++) {
  340. const overlay = Array.isArray(overlays[i]) ? overlays[i][0] : overlays[i];
  341. const itemId = AMapType.includes(props.activeMap) ? overlay?.getExtData().id : overlay.get('id');
  342. if (itemId === id) {
  343. removeOverlayByIndex(i);
  344. commit(deepClone(overlaysData));
  345. rightClickObj = null;
  346. }
  347. }
  348. }
  349. };
  350. // 撤销绘制
  351. const handleUndo = () => {
  352. mouseToolState.drawing = false;
  353. const previous = history.value[history.value.length - 2];
  354. if (history.value.length > 1) {
  355. if (currentState.value.length > previous.length) {
  356. // 撤销新增
  357. removeOverlayByIndex(currentState.value.length - 1);
  358. } else {
  359. let restoreData;
  360. for (let i = 0; i < previous.length; i++) {
  361. let index = 0;
  362. for (let k = 0; k < currentState.value.length; k++) {
  363. if (previous[i].id !== currentState.value[k].id) {
  364. index++;
  365. } else {
  366. break;
  367. }
  368. }
  369. if (index === previous.length - 1) {
  370. restoreData = previous[i];
  371. break;
  372. }
  373. }
  374. if (restoreData) {
  375. const newData = {
  376. type: restoreData.type,
  377. path: restoreData.path,
  378. strokeColor: restoreData.color,
  379. strokeOpacity: 1,
  380. strokeWeight: '1',
  381. fillColor: restoreData.color,
  382. fillOpacity: restoreData.drawType === '1' ? '0' : '0.5'
  383. };
  384. if (restoreData.type === 'circle') {
  385. newData.center = restoreData.center;
  386. newData.radius = restoreData.radius;
  387. }
  388. let obj;
  389. if (AMapType.includes(props.activeMap)) {
  390. obj = getDrawTool().createGraphics(newData);
  391. } else {
  392. obj = getDrawTool().createGraphics(newData);
  393. }
  394. if (!!obj) {
  395. overlays.push(obj);
  396. }
  397. }
  398. }
  399. undo();
  400. console.log(history.value, future.value, currentState.value);
  401. }
  402. };
  403. // 根据索引移除覆盖物
  404. const removeOverlayByIndex = (index: number) => {
  405. const map = getMap();
  406. if (AMapType.includes(props.activeMap)) {
  407. if (Array.isArray(overlays[index])) {
  408. overlays[index].forEach((overlay) => {
  409. // 移除地图上覆盖物
  410. map.remove(overlay);
  411. });
  412. } else {
  413. // 移除地图上覆盖物
  414. map.remove(overlays[index]);
  415. }
  416. } else {
  417. if (Array.isArray(overlays[index])) {
  418. overlays[index].forEach((overlay) => {
  419. // 移除地图上覆盖物
  420. mapUtils.value.getDrawVector().getSource().removeFeature(overlay);
  421. });
  422. } else {
  423. // 移除地图上覆盖物
  424. mapUtils.value.getDrawVector().getSource().removeFeature(overlays[index]);
  425. }
  426. }
  427. overlays.splice(index, 1);
  428. overlaysData.splice(index, 1);
  429. };
  430. let selectedScope = reactive({});
  431. // 空间分析数据
  432. const analysisSpatial = (data) => {
  433. // 已选中的范围
  434. if (selectedScope[data.id]) {
  435. delete selectedScope[data.id];
  436. if (JSON.stringify(selectedScope) === '{}') {
  437. // rightMenuRef.value.clickContractMenu();
  438. return;
  439. }
  440. } else {
  441. selectedScope[data.id] = data;
  442. }
  443. let location = [];
  444. for (let key in selectedScope) {
  445. let itemLocation = [];
  446. if (selectedScope[key].path && selectedScope[key].path.length > 1) {
  447. selectedScope[key].path.forEach((item) => {
  448. itemLocation.push({
  449. x: item[0],
  450. y: item[1]
  451. });
  452. });
  453. itemLocation.push(itemLocation[0]);
  454. location.push(itemLocation);
  455. }
  456. }
  457. emits('handleAnalysisData', location);
  458. };
  459. const onMapClick = (event) => {
  460. // 没在编辑时
  461. if (!mouseToolState.drawing && event.pixel) {
  462. const feature = map.value.forEachFeatureAtPixel(event.pixel, (feature2) => {
  463. return feature2;
  464. });
  465. const data = feature.get('extraData');
  466. analysisSpatial(data);
  467. }
  468. unMapClick(event);
  469. };
  470. const unMapClick = (event) => {
  471. const overlayElement = popup?.getElement();
  472. const isClickOnOverlay =
  473. event && overlayElement && (overlayElement.contains(event.originalEvent.target) || overlayElement === event.originalEvent.target);
  474. if (!isClickOnOverlay && popup) {
  475. map.value.removeOverlay(popup);
  476. popup = undefined;
  477. }
  478. };
  479. onMounted(() => {
  480. if (!AMapType.includes(props.activeMap)) {
  481. map.value.on('click', onMapClick);
  482. }
  483. });
  484. onBeforeUnmount(() => {
  485. if (overlays && overlays.length > 0) {
  486. overlays.forEach((overlay) => {
  487. if (AMapType.includes(props.activeMap)) {
  488. overlay.setMap(null);
  489. } else {
  490. mapUtils.value.getDrawVector().getSource().removeFeature(overlay);
  491. }
  492. });
  493. }
  494. if (!AMapType.includes(props.activeMap)) {
  495. map.value.un('click', onMapClick);
  496. }
  497. });
  498. </script>
  499. <style lang="scss" scoped>
  500. .draw-tools-container {
  501. position: absolute;
  502. right: 65px;
  503. bottom: 177px;
  504. background-color: #304468;
  505. border-radius: 5px;
  506. display: flex;
  507. color: #fff;
  508. font-size: 14px;
  509. padding: 8px 0;
  510. .draw-item {
  511. display: flex;
  512. align-items: center;
  513. position: relative;
  514. padding: 0 10px;
  515. cursor: pointer;
  516. border-left: 1px solid #3a74be;
  517. &:first-child {
  518. border-left: none;
  519. }
  520. .item-label {
  521. margin-right: 10px;
  522. }
  523. .select-box {
  524. position: absolute;
  525. left: 0;
  526. z-index: 7;
  527. width: 100%;
  528. .box-content {
  529. background-color: #102341;
  530. }
  531. .select-item {
  532. display: flex;
  533. align-items: center;
  534. cursor: pointer;
  535. padding: 4px 10px;
  536. &:hover {
  537. background-color: rgba(22, 73, 142, 0.5);
  538. }
  539. &:first-child {
  540. margin-left: 0;
  541. }
  542. }
  543. .color-box {
  544. margin-right: 5px;
  545. }
  546. }
  547. }
  548. .color-box {
  549. display: block;
  550. width: 20px;
  551. height: 20px;
  552. cursor: pointer;
  553. border-radius: 3px;
  554. }
  555. }
  556. .active {
  557. background-color: rgba(22, 73, 142, 0.5);
  558. }
  559. </style>