Cmen
3 years ago
18 changed files with 381 additions and 360 deletions
@ -0,0 +1,203 @@ |
|||||||
|
import Emitter from "@finevis/emitter"; |
||||||
|
import "@amap/amap-jsapi-types"; |
||||||
|
|
||||||
|
import { getOverlayPaths, getUID, registerHotkey } from "@utils"; |
||||||
|
|
||||||
|
import { |
||||||
|
BaseOverlayEditor, |
||||||
|
RectangleEditor, |
||||||
|
PolygonEditor, |
||||||
|
PolylineEditor, |
||||||
|
CircleEditor, |
||||||
|
} from "./editors"; |
||||||
|
|
||||||
|
import { IMapOptions, IMapEditor } from "./type"; |
||||||
|
|
||||||
|
import { PolygonOptions, PolylineOptions, SelectedOptions } from "./constants"; |
||||||
|
|
||||||
|
import { EventTypes, OverlayTypes } from "../types/enum"; |
||||||
|
|
||||||
|
type AMapOverlayEditor = |
||||||
|
| AMap.RectangleEditor |
||||||
|
| AMap.PolygonEditor |
||||||
|
| AMap.PolylineEditor |
||||||
|
| AMap.CircleEditor; |
||||||
|
|
||||||
|
type OverlayTemp = { |
||||||
|
type: OverlayTypes; |
||||||
|
target: AMap.MapOverlay; |
||||||
|
}; |
||||||
|
|
||||||
|
export class MapEditor extends Emitter implements IMapEditor { |
||||||
|
dom: HTMLDivElement; |
||||||
|
private _map: AMap.Map | undefined; |
||||||
|
private overlayEditors: BaseOverlayEditor<AMapOverlayEditor>[] = []; |
||||||
|
private overlayMap: Record<string, OverlayTemp> = {}; |
||||||
|
private currentOverlayEditor: |
||||||
|
| BaseOverlayEditor<AMapOverlayEditor> |
||||||
|
| undefined; |
||||||
|
private selectedIds: string[] = []; |
||||||
|
private editorStatus: "editing" | "creating" | null = null; |
||||||
|
constructor(dom: HTMLDivElement) { |
||||||
|
super(); |
||||||
|
this.dom = dom; |
||||||
|
} |
||||||
|
|
||||||
|
get map() { |
||||||
|
return this._map!; |
||||||
|
} |
||||||
|
|
||||||
|
async init() { |
||||||
|
await AMapLoader.load({ |
||||||
|
key: "a4171ad2d7df42823b4de7d25c8c35ee", |
||||||
|
version: "2.0", |
||||||
|
plugins: [ |
||||||
|
"AMap.RectangleEditor", |
||||||
|
"AMap.PolylineEditor", |
||||||
|
"AMap.PolygonEditor", |
||||||
|
"AMap.CircleEditor", |
||||||
|
"AMap.PlaceSearch", |
||||||
|
"AMap.AutoComplete", |
||||||
|
], |
||||||
|
}); |
||||||
|
this._map = new AMap.Map(this.dom); |
||||||
|
this.initEditors(); |
||||||
|
// Space的key是空字符串, 这就离谱.
|
||||||
|
registerHotkey(" ", { callback: this.finishEditOverlay.bind(this) }); |
||||||
|
registerHotkey("e", { |
||||||
|
callback: this.editSelectedOverlay.bind(this), |
||||||
|
alt: true, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
initEditors() { |
||||||
|
const { map } = this; |
||||||
|
this.overlayEditors = [ |
||||||
|
new RectangleEditor(map), |
||||||
|
new PolygonEditor(map), |
||||||
|
new PolylineEditor(map), |
||||||
|
new CircleEditor(map), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
getEditorByType(overlayType: OverlayTypes) { |
||||||
|
return this.overlayEditors.find( |
||||||
|
(editor) => editor.getType() === overlayType |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
importProject(options: IMapOptions) { |
||||||
|
this.clearOverlays(); |
||||||
|
const { rectangles, polygons, polylines, circles } = options; |
||||||
|
const rectangleEditor = this.getEditorByType(OverlayTypes.Rectangle); |
||||||
|
const polygonEditor = this.getEditorByType(OverlayTypes.Polygon); |
||||||
|
const polylineEditor = this.getEditorByType(OverlayTypes.Polyline); |
||||||
|
const circleEditor = this.getEditorByType(OverlayTypes.Circle); |
||||||
|
|
||||||
|
const addOverlay = ( |
||||||
|
id: string, |
||||||
|
target: AMap.MapOverlay, |
||||||
|
type: OverlayTypes |
||||||
|
) => { |
||||||
|
this.overlayMap[id] = { |
||||||
|
type, |
||||||
|
target, |
||||||
|
}; |
||||||
|
this.map.add(target); |
||||||
|
}; |
||||||
|
|
||||||
|
rectangles.forEach((rect) => |
||||||
|
addOverlay(rect.id, rectangleEditor!.build(rect), OverlayTypes.Rectangle) |
||||||
|
); |
||||||
|
polygons.forEach((polygon) => |
||||||
|
addOverlay( |
||||||
|
polygon.id, |
||||||
|
polygonEditor!.build(polygon), |
||||||
|
OverlayTypes.Polygon |
||||||
|
) |
||||||
|
); |
||||||
|
polylines.forEach((polyline) => |
||||||
|
addOverlay( |
||||||
|
polyline.id, |
||||||
|
polylineEditor!.build(polyline), |
||||||
|
OverlayTypes.Polyline |
||||||
|
) |
||||||
|
); |
||||||
|
circles.forEach((circle) => |
||||||
|
addOverlay(circle.id, circleEditor!.build(circle), OverlayTypes.Circle) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
importGeoJSON(geojson: GeoJSON.FeatureCollection): void { |
||||||
|
throw new Error("Method not implemented."); |
||||||
|
} |
||||||
|
|
||||||
|
// 清空所有覆盖物
|
||||||
|
clearOverlays() { |
||||||
|
for (let id in this.overlayMap) { |
||||||
|
this.map.remove(this.overlayMap[id].target); |
||||||
|
} |
||||||
|
this.overlayMap = {}; |
||||||
|
} |
||||||
|
|
||||||
|
createOverlay(type: OverlayTypes) { |
||||||
|
this.currentOverlayEditor = this.getEditorByType(type!); |
||||||
|
this.currentOverlayEditor?.create(); |
||||||
|
this.editorStatus = "creating"; |
||||||
|
} |
||||||
|
|
||||||
|
selectOverlays(ids?: string[]) { |
||||||
|
if (!ids?.length) return; |
||||||
|
this.selectedIds = ids; |
||||||
|
ids.forEach((id) => { |
||||||
|
const { target } = this.overlayMap[id]; |
||||||
|
target.setOptions(SelectedOptions); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
deleteOverlays() { |
||||||
|
if (this.selectedIds?.length === 0) return; |
||||||
|
this.selectedIds.forEach((id) => { |
||||||
|
const { target } = this.overlayMap[id]; |
||||||
|
this.map.remove(target); |
||||||
|
delete this.overlayMap[id]; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
finishEditOverlay() { |
||||||
|
if (this.currentOverlayEditor == null) return; |
||||||
|
const target = this.currentOverlayEditor.finish(); |
||||||
|
if (target == null) return; |
||||||
|
const type = this.currentOverlayEditor.getType(); |
||||||
|
const isCreatingOverlay = this.editorStatus === "creating"; |
||||||
|
let id = getUID(); |
||||||
|
if (isCreatingOverlay) { |
||||||
|
this.overlayMap[id] = { type, target }; |
||||||
|
} else { |
||||||
|
id = this.selectedIds[0]; |
||||||
|
} |
||||||
|
const evt: any = { id, type }; |
||||||
|
if (type === OverlayTypes.Circle) { |
||||||
|
const circle = target as AMap.Circle; |
||||||
|
const { lng, lat } = circle.getCenter(); |
||||||
|
evt.lngLat = [lng, lat]; |
||||||
|
evt.radius = circle.getRadius(); |
||||||
|
} else { |
||||||
|
evt.path = getOverlayPaths(target, type); |
||||||
|
} |
||||||
|
target.setOptions( |
||||||
|
type === OverlayTypes.Polyline ? PolylineOptions : PolygonOptions |
||||||
|
); |
||||||
|
this.emit(EventTypes.FinishEditOverlay, evt); |
||||||
|
this.editorStatus = null; |
||||||
|
} |
||||||
|
|
||||||
|
editSelectedOverlay() { |
||||||
|
if (this.selectedIds.length !== 1) return; |
||||||
|
const [id] = this.selectedIds; |
||||||
|
const { target, type } = this.overlayMap[id]; |
||||||
|
this.currentOverlayEditor = this.getEditorByType(type); |
||||||
|
this.currentOverlayEditor?.edit(target); |
||||||
|
this.editorStatus = "editing"; |
||||||
|
} |
||||||
|
} |
@ -1,221 +1,2 @@ |
|||||||
import Emitter from "@finevis/emitter"; |
export * from "./MapEditor"; |
||||||
import "@amap/amap-jsapi-types"; |
export * from "./type"; |
||||||
|
|
||||||
import { IMapState, IOverlay } from "@store"; |
|
||||||
import { getOverlayPaths, getUID } from "@utils"; |
|
||||||
|
|
||||||
import { |
|
||||||
BaseOverlayEditor, |
|
||||||
RectangleEditor, |
|
||||||
PolygonEditor, |
|
||||||
PolylineEditor, |
|
||||||
CircleEditor, |
|
||||||
} from "./editors"; |
|
||||||
|
|
||||||
import { registerHotkey } from "../utils/hotkeys"; |
|
||||||
import { PolygonOptions, PolylineOptions, SelectedOptions } from "./constants"; |
|
||||||
|
|
||||||
import { Command, EventTypes, OverlayTypes } from "../types/enum"; |
|
||||||
|
|
||||||
type AMapOverlayEditor = |
|
||||||
| AMap.RectangleEditor |
|
||||||
| AMap.PolygonEditor |
|
||||||
| AMap.PolylineEditor |
|
||||||
| AMap.CircleEditor; |
|
||||||
|
|
||||||
type OverlayTemp = { |
|
||||||
type: OverlayTypes; |
|
||||||
target: AMap.MapOverlay; |
|
||||||
}; |
|
||||||
|
|
||||||
export class MapEditor extends Emitter { |
|
||||||
dom: HTMLDivElement; |
|
||||||
private _map: AMap.Map | undefined; |
|
||||||
private overlayEditors: BaseOverlayEditor<AMapOverlayEditor>[] = []; |
|
||||||
private overlayMap: Record<string, OverlayTemp> = {}; |
|
||||||
private _currentOverlayEditor: |
|
||||||
| BaseOverlayEditor<AMapOverlayEditor> |
|
||||||
| undefined; |
|
||||||
private _selectedIds: string[] = []; |
|
||||||
private _editorStatus: "editing" | "creating" | null = null; |
|
||||||
constructor(dom: HTMLDivElement) { |
|
||||||
super(); |
|
||||||
this.dom = dom; |
|
||||||
} |
|
||||||
|
|
||||||
get map() { |
|
||||||
return this._map!; |
|
||||||
} |
|
||||||
|
|
||||||
async init() { |
|
||||||
await AMapLoader.load({ |
|
||||||
key: "a4171ad2d7df42823b4de7d25c8c35ee", |
|
||||||
version: "2.0", |
|
||||||
plugins: [ |
|
||||||
"AMap.RectangleEditor", |
|
||||||
"AMap.PolylineEditor", |
|
||||||
"AMap.PolygonEditor", |
|
||||||
"AMap.CircleEditor", |
|
||||||
"AMap.PlaceSearch", |
|
||||||
"AMap.AutoComplete", |
|
||||||
], |
|
||||||
}); |
|
||||||
this._map = new AMap.Map(this.dom); |
|
||||||
this.initEditors(); |
|
||||||
// Space的key是空字符串, 这就离谱.
|
|
||||||
registerHotkey(" ", { callback: this._finishEditOverlay.bind(this) }); |
|
||||||
registerHotkey("e", { |
|
||||||
callback: this._editSelectedOverlay.bind(this), |
|
||||||
alt: true, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
update(mapState: IMapState) { |
|
||||||
if (this._editorStatus != null) { |
|
||||||
this._finishEditOverlay(); |
|
||||||
} |
|
||||||
const { command } = mapState; |
|
||||||
switch (command) { |
|
||||||
case Command.CreateOverlay: |
|
||||||
this._createOverlay(mapState); |
|
||||||
break; |
|
||||||
case Command.SelectOverlay: |
|
||||||
this._selectOverlays(mapState.selectedIds); |
|
||||||
break; |
|
||||||
case Command.DeleteOverlays: |
|
||||||
this._deleteOverlays(); |
|
||||||
break; |
|
||||||
case Command.OpenProject: |
|
||||||
this.importOverlays(mapState); |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
initEditors() { |
|
||||||
const { map } = this; |
|
||||||
this.overlayEditors = [ |
|
||||||
new RectangleEditor(map), |
|
||||||
new PolygonEditor(map), |
|
||||||
new PolylineEditor(map), |
|
||||||
new CircleEditor(map), |
|
||||||
]; |
|
||||||
} |
|
||||||
|
|
||||||
getEditorByType(overlayType: OverlayTypes) { |
|
||||||
return this.overlayEditors.find( |
|
||||||
(editor) => editor.getType() === overlayType |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
importOverlays(mapState: IMapState) { |
|
||||||
this.clearOverlays(); |
|
||||||
const { rectangles, polygons, polylines, circles } = mapState; |
|
||||||
const rectangleEditor = this.getEditorByType(OverlayTypes.Rectangle); |
|
||||||
const polygonEditor = this.getEditorByType(OverlayTypes.Polygon); |
|
||||||
const polylineEditor = this.getEditorByType(OverlayTypes.Polyline); |
|
||||||
const circleEditor = this.getEditorByType(OverlayTypes.Circle); |
|
||||||
|
|
||||||
const addOverlay = ( |
|
||||||
id: string, |
|
||||||
target: AMap.MapOverlay, |
|
||||||
type: OverlayTypes |
|
||||||
) => { |
|
||||||
this.overlayMap[id] = { |
|
||||||
type, |
|
||||||
target, |
|
||||||
}; |
|
||||||
this.map.add(target); |
|
||||||
}; |
|
||||||
|
|
||||||
rectangles.forEach((rect) => |
|
||||||
addOverlay(rect.id, rectangleEditor!.build(rect), OverlayTypes.Rectangle) |
|
||||||
); |
|
||||||
polygons.forEach((polygon) => |
|
||||||
addOverlay( |
|
||||||
polygon.id, |
|
||||||
polygonEditor!.build(polygon), |
|
||||||
OverlayTypes.Polygon |
|
||||||
) |
|
||||||
); |
|
||||||
polylines.forEach((polyline) => |
|
||||||
addOverlay( |
|
||||||
polyline.id, |
|
||||||
polylineEditor!.build(polyline), |
|
||||||
OverlayTypes.Polyline |
|
||||||
) |
|
||||||
); |
|
||||||
circles.forEach((circle) => |
|
||||||
addOverlay(circle.id, circleEditor!.build(circle), OverlayTypes.Circle) |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 清空所有覆盖物
|
|
||||||
clearOverlays() { |
|
||||||
for (let id in this.overlayMap) { |
|
||||||
this.map.remove(this.overlayMap[id].target); |
|
||||||
} |
|
||||||
this.overlayMap = {}; |
|
||||||
} |
|
||||||
|
|
||||||
_createOverlay(mapState: IMapState) { |
|
||||||
const { overlayType } = mapState; |
|
||||||
this._currentOverlayEditor = this.getEditorByType(overlayType!); |
|
||||||
this._currentOverlayEditor?.create(); |
|
||||||
this._editorStatus = "creating"; |
|
||||||
} |
|
||||||
|
|
||||||
_selectOverlays(ids?: string[]) { |
|
||||||
if (!ids?.length) return; |
|
||||||
this._selectedIds = ids; |
|
||||||
ids.forEach((id) => { |
|
||||||
const { target } = this.overlayMap[id]; |
|
||||||
target.setOptions(SelectedOptions); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
_deleteOverlays() { |
|
||||||
if (this._selectedIds?.length === 0) return; |
|
||||||
this._selectedIds.forEach((id) => { |
|
||||||
const { target } = this.overlayMap[id]; |
|
||||||
this.map.remove(target); |
|
||||||
delete this.overlayMap[id]; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
_finishEditOverlay() { |
|
||||||
if (this._currentOverlayEditor == null) return; |
|
||||||
const target = this._currentOverlayEditor.finish(); |
|
||||||
if (target == null) return; |
|
||||||
const type = this._currentOverlayEditor.getType(); |
|
||||||
const isCreatingOverlay = this._editorStatus === "creating"; |
|
||||||
let id = getUID(); |
|
||||||
if (isCreatingOverlay) { |
|
||||||
this.overlayMap[id] = { type, target }; |
|
||||||
} else { |
|
||||||
id = this._selectedIds[0]; |
|
||||||
} |
|
||||||
const evt: any = { id, type }; |
|
||||||
if (type === OverlayTypes.Circle) { |
|
||||||
const circle = target as AMap.Circle; |
|
||||||
const { lng, lat } = circle.getCenter(); |
|
||||||
evt.lngLat = [lng, lat]; |
|
||||||
evt.radius = circle.getRadius(); |
|
||||||
} else { |
|
||||||
evt.path = getOverlayPaths(target, type); |
|
||||||
} |
|
||||||
target.setOptions( |
|
||||||
type === OverlayTypes.Polyline ? PolylineOptions : PolygonOptions |
|
||||||
); |
|
||||||
this.emit(EventTypes.FinishEditOverlay, evt); |
|
||||||
this._editorStatus = null; |
|
||||||
} |
|
||||||
|
|
||||||
_editSelectedOverlay() { |
|
||||||
if (this._selectedIds.length !== 1) return; |
|
||||||
const [id] = this._selectedIds; |
|
||||||
const { target, type } = this.overlayMap[id]; |
|
||||||
this._currentOverlayEditor = this.getEditorByType(type); |
|
||||||
this._currentOverlayEditor?.edit(target); |
|
||||||
this._editorStatus = "editing"; |
|
||||||
} |
|
||||||
} |
|
||||||
|
@ -0,0 +1,26 @@ |
|||||||
|
import { EventTypes, OverlayTypes } from "@types"; |
||||||
|
|
||||||
|
export interface IMapEditor { |
||||||
|
on(type: EventTypes, evt: any): void; |
||||||
|
init(): Promise<void>; |
||||||
|
createOverlay(type: OverlayTypes): void; |
||||||
|
importProject(options: IMapOptions): void; |
||||||
|
importGeoJSON(geojson: GeoJSON.FeatureCollection): void; |
||||||
|
selectOverlays(ids: string[]): void; |
||||||
|
} |
||||||
|
|
||||||
|
export interface IOverlay { |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
type: OverlayTypes; |
||||||
|
lngLat?: GeoJSON.Position; |
||||||
|
path?: GeoJSON.Position[]; |
||||||
|
radius?: number; |
||||||
|
} |
||||||
|
export interface IMapOptions { |
||||||
|
polygons: IOverlay[]; |
||||||
|
polylines: IOverlay[]; |
||||||
|
circles: IOverlay[]; |
||||||
|
rectangles: IOverlay[]; |
||||||
|
selectedIds?: string[]; |
||||||
|
} |
@ -1,3 +1,5 @@ |
|||||||
import { IEditorState } from "@store"; |
import { IEditorState } from "@store"; |
||||||
|
|
||||||
export const mapStateSelector = (state: IEditorState) => state.map; |
export const mapOptionsSelector = (state: IEditorState) => state.map; |
||||||
|
export const overlayTypeSelector = (state: IEditorState) => state.overlayType; |
||||||
|
export const statusSelector = (state: IEditorState) => state.status; |
||||||
|
Loading…
Reference in new issue