import { Map as OLMap } from 'ol'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import { LineString, Polygon } from 'ol/geom'; import { Feature } from 'ol'; import { Style, Stroke, Fill } from 'ol/style'; import GeoJSON from 'ol/format/GeoJSON'; import { polygon as turfPolygon, booleanIntersects } from '@turf/turf'; import { toLonLat } from 'ol/proj'; import DragPan from 'ol/interaction/DragPan'; import MouseWheelZoom from 'ol/interaction/MouseWheelZoom'; export class LassoSelector { private map: OLMap; private drawLayer: VectorLayer; private drawSource: VectorSource; private overlaySource: VectorSource; private overlayLayer: VectorLayer; private drawing = false; private coordinates: [number, number][] = []; private targetSource: VectorSource; private isShiftKeyDown = false; private onSelectCallback: (selected: Feature[], isInvert?: boolean) => void; private dragPanInteraction: DragPan | null = null; private mouseWheelZoomInteraction: MouseWheelZoom | null = null; constructor( map: OLMap, targetSource: VectorSource, onSelect: (selected: Feature[], isInvert?: boolean) => void ) { this.map = map; this.targetSource = targetSource; this.onSelectCallback = onSelect; // 找出拖动和滚轮缩放交互 this.dragPanInteraction = this.map .getInteractions() .getArray() .find((interaction) => interaction instanceof DragPan) as DragPan; this.mouseWheelZoomInteraction = this.map .getInteractions() .getArray() .find((interaction) => interaction instanceof MouseWheelZoom) as MouseWheelZoom; this.drawSource = new VectorSource(); this.drawLayer = new VectorLayer({ source: this.drawSource, style: new Style({ stroke: new Stroke({ color: '#ff0000', width: 2, }), }), }); this.map.addLayer(this.drawLayer); this.overlaySource = new VectorSource(); this.overlayLayer = new VectorLayer({ source: this.overlaySource, style: new Style({ stroke: new Stroke({ color: 'rgba(255, 0, 0, 0.8)', width: 2, }), fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)', }), }), }); this.map.addLayer(this.overlayLayer); this.bindEvents(); } private bindEvents() { // 禁用默认右键菜单 this.map.getViewport().addEventListener('contextmenu', (e) => e.preventDefault()); // pointerdown 捕获左键按下 this.map.getViewport().addEventListener('pointerdown', (e) => { if (e.button === 0 && !this.drawing) { e.preventDefault(); e.stopPropagation(); this.isShiftKeyDown = e.shiftKey; this.drawing = true; this.coordinates = []; this.drawSource.clear(); this.overlaySource.clear(); // 禁用拖动和缩放 if (this.dragPanInteraction) this.dragPanInteraction.setActive(false); if (this.mouseWheelZoomInteraction) this.mouseWheelZoomInteraction.setActive(false); } }); // pointermove 画线 this.map.on('pointermove', (evt) => { if (!this.drawing) return; const coord = evt.coordinate as [number, number]; this.coordinates.push(coord); this.renderLine(); this.renderPolygon(); }); // pointerup 捕获左键抬起 this.map.getViewport().addEventListener('pointerup', (e) => { if (e.button === 0 && this.drawing) { e.preventDefault(); e.stopPropagation(); this.drawing = false; this.handleDrawEnd(); // 恢复拖动和缩放 if (this.dragPanInteraction) this.dragPanInteraction.setActive(true); if (this.mouseWheelZoomInteraction) this.mouseWheelZoomInteraction.setActive(true); } }); // 防止拖动导致意外事件 this.map.getViewport().addEventListener('pointercancel', (e) => { if (this.drawing) { this.drawing = false; this.drawSource.clear(); this.overlaySource.clear(); if (this.dragPanInteraction) this.dragPanInteraction.setActive(true); if (this.mouseWheelZoomInteraction) this.mouseWheelZoomInteraction.setActive(true); } }); } private renderLine() { this.drawSource.clear(); if (this.coordinates.length >= 2) { const line = new LineString(this.coordinates); const feature = new Feature({ geometry: line }); this.drawSource.addFeature(feature); } } private renderPolygon() { this.overlaySource.clear(); if (this.coordinates.length < 3) return; const polygonCoords = [...this.coordinates, this.coordinates[0]]; const polygon = new Polygon([polygonCoords]); const feature = new Feature({ geometry: polygon }); this.overlaySource.addFeature(feature); } private handleDrawEnd() { if (this.coordinates.length < 3) { this.drawSource.clear(); this.overlaySource.clear(); return; } const first = this.coordinates[0]; const last = this.coordinates[this.coordinates.length - 1]; if (first[0] !== last[0] || first[1] !== last[1]) { this.coordinates.push([...first]); } const coords4326 = this.coordinates.map((c) => toLonLat(c)); const turfPoly = turfPolygon([coords4326]); const geojson = new GeoJSON(); const selected: Feature[] = []; this.targetSource.getFeatures().forEach((feature) => { const geom = feature.getGeometry(); if (!geom) return; const geomObj = geojson.writeGeometryObject(geom, { featureProjection: 'EPSG:3857', dataProjection: 'EPSG:4326', }) as any; if ( (geomObj.type === 'Polygon' || geomObj.type === 'MultiPolygon') && booleanIntersects(turfPoly, geomObj) ) { selected.push(feature); } }); if (selected.length) { this.onSelectCallback(selected, this.isShiftKeyDown); } this.drawSource.clear(); this.overlaySource.clear(); } destroy() { this.map.removeLayer(this.drawLayer); this.map.removeLayer(this.overlayLayer); } }