├── src ├── vite-env.d.ts ├── assets │ └── circle_red.png ├── interface.ts ├── events.ts ├── line │ ├── freehand-line.ts │ └── curve.ts ├── polygon │ ├── polygon.ts │ ├── triangle.ts │ ├── freehand-polygon.ts │ ├── rectangle.ts │ ├── sector.ts │ ├── circle.ts │ ├── ellipse.ts │ └── lune.ts ├── arrow │ ├── assault-direction.ts │ ├── straight-arrow.ts │ ├── swallowtail-squad-combat.ts │ ├── swallowtail-attack-arrow.ts │ ├── squad-combat.ts │ ├── fine-arrow.ts │ ├── curved-arrow.ts │ ├── attack-arrow.ts │ └── double-arrow.ts ├── index.ts ├── utils.ts └── base.ts ├── examples ├── banner.png ├── attack-arrow-growth.gif ├── fine-arrow-growth.gif ├── show-hide-animation.gif ├── index.html └── index.js ├── .prettierrc.cjs ├── .eslintrc.js ├── tsconfig.node.json ├── .gitignore ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── package.json ├── CHANGELOG.md ├── index.html ├── debug └── index.ts └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-zf/cesium-plot-js/HEAD/examples/banner.png -------------------------------------------------------------------------------- /src/assets/circle_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-zf/cesium-plot-js/HEAD/src/assets/circle_red.png -------------------------------------------------------------------------------- /examples/attack-arrow-growth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-zf/cesium-plot-js/HEAD/examples/attack-arrow-growth.gif -------------------------------------------------------------------------------- /examples/fine-arrow-growth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-zf/cesium-plot-js/HEAD/examples/fine-arrow-growth.gif -------------------------------------------------------------------------------- /examples/show-hide-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-zf/cesium-plot-js/HEAD/examples/show-hide-animation.gif -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 5 | ignorePatterns: ['dist', '.eslintrc.cjs'], 6 | parser: '@typescript-eslint/parser', 7 | rules: {}, 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "paths": { 9 | "@examples/*": ["debug/*"] 10 | } 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | // "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "paths": { 23 | "@examples/*": ["debug/*"] 24 | }, 25 | }, 26 | // "include": ["src", "examples"], 27 | "references": [{ "path": "./tsconfig.node.json" }], 28 | "exclude": ["node_modules", "debug/cesium/*", "@examples/cesium/*"] 29 | } 30 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as CesiumTypeOnly from 'cesium'; 3 | 4 | export type PolygonStyle = { 5 | material?: CesiumTypeOnly.MaterialProperty | CesiumTypeOnly.Color; 6 | outlineWidth?: number; 7 | outlineMaterial?: CesiumTypeOnly.MaterialProperty | CesiumTypeOnly.Color; 8 | }; 9 | 10 | export type LineStyle = { 11 | material?: CesiumTypeOnly.Color; 12 | lineWidth?: number; 13 | }; 14 | 15 | export type State = 'drawing' | 'edit' | 'static' | 'animating' | 'hidden'; 16 | export type GeometryStyle = PolygonStyle | LineStyle; 17 | 18 | export type EventType = 'drawStart' | 'drawUpdate' | 'drawEnd' | 'editEnd' | 'editStart'; 19 | export type EventListener = (eventData?: any) => void; 20 | 21 | export type VisibleAnimationOpts = { 22 | duration?: number; 23 | delay?: number; 24 | callback?: () => void; 25 | }; 26 | 27 | export type GrowthAnimationOpts = { 28 | duration: number; 29 | delay: number; 30 | callback: Function; 31 | }; 32 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | let config = {}; 6 | if (mode === 'dev') { 7 | config = { 8 | base: './', 9 | define: { 10 | 'process.env': { 11 | NODE_ENV: mode, 12 | }, 13 | }, 14 | server: { 15 | port: 3001, 16 | open: true, 17 | }, 18 | }; 19 | } else if (mode === 'prod') { 20 | config = { 21 | build: { 22 | lib: { 23 | entry: path.resolve(__dirname, 'src/index.ts'), 24 | name: 'CesiumPlot', 25 | fileName: 'CesiumPlot', 26 | }, 27 | rollupOptions: { 28 | external: ['cesium'], 29 | }, 30 | }, 31 | }; 32 | } 33 | 34 | config.resolve = { 35 | alias: { 36 | '@': path.resolve(__dirname, 'src'), 37 | '@examples': path.resolve(__dirname, './examples'), 38 | }, 39 | }; 40 | return config; 41 | }); 42 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import { EventType, EventListener } from './interface'; 2 | 3 | export default class EventDispatcher { 4 | listeners: Map>; 5 | 6 | constructor() { 7 | this.listeners = new Map([ 8 | ['drawStart', new Set()], 9 | ['drawUpdate', new Set()], 10 | ['drawEnd', new Set()], 11 | ['editStart', new Set()], 12 | ['editEnd', new Set()], 13 | ]); 14 | } 15 | 16 | on(event: EventType, listener: EventListener) { 17 | if (!this.listeners.has(event)) { 18 | console.warn("Event binding must be one of 'drawStart', 'drawUpdate', or 'drawEnd'."); 19 | return; 20 | } 21 | this.listeners.get(event)!.add(listener); 22 | } 23 | 24 | off(event: EventType, listener: EventListener) { 25 | if (this.listeners.has(event)) { 26 | this.listeners.get(event)!.delete(listener); 27 | } 28 | } 29 | 30 | dispatchEvent(event: EventType, eventData?: any) { 31 | if (this.listeners.has(event)) { 32 | this.listeners.get(event)!.forEach((listener) => { 33 | listener(eventData); 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ethan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cesium-plot-js", 3 | "version": "0.0.6", 4 | "main": "dist/CesiumPlot.umd.js", 5 | "homepage": "https://github.com/ethan-zf/cesium-plot-js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:ethan-zf/cesium-plot-js.git" 9 | }, 10 | "files": [ 11 | "dist/", 12 | "README.md", 13 | "package.json" 14 | ], 15 | "keywords": [ 16 | "cesium-plot", 17 | "cesium-draw", 18 | "cesium", 19 | "draw", 20 | "plot", 21 | "map" 22 | ], 23 | "scripts": { 24 | "dev": "vite --mode dev", 25 | "build": "vite build --mode prod", 26 | "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", 27 | "preview": "vite preview" 28 | }, 29 | "devDependencies": { 30 | "@types/lodash.clonedeep": "^4.5.9", 31 | "@types/lodash.merge": "^4.6.9", 32 | "@typescript-eslint/eslint-plugin": "^6.0.0", 33 | "@typescript-eslint/parser": "^6.0.0", 34 | "cesium": "1.99.0", 35 | "eslint": "^8.45.0", 36 | "typescript": "^5.0.2", 37 | "vite": "^4.4.5" 38 | }, 39 | "dependencies": { 40 | "lodash.clonedeep": "^4.5.0", 41 | "lodash.merge": "^4.6.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/line/freehand-line.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | import { PolygonStyle } from '../interface'; 5 | 6 | export default class FreehandLine extends Base { 7 | points: Cartesian3[] = []; 8 | freehand: boolean; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.freehand = true; 14 | this.setState('drawing'); 15 | } 16 | 17 | getType(): 'polygon' | 'line' { 18 | return 'line'; 19 | } 20 | 21 | /** 22 | * Add points only on click events 23 | */ 24 | addPoint(cartesian: Cartesian3) { 25 | this.points.push(cartesian); 26 | if (this.points.length < 2) { 27 | this.onMouseMove(); 28 | } else { 29 | this.finishDrawing(); 30 | } 31 | } 32 | 33 | /** 34 | * Draw a shape based on mouse movement points during the initial drawing. 35 | */ 36 | updateMovingPoint(cartesian: Cartesian3) { 37 | this.points.push(cartesian); 38 | this.setGeometryPoints(this.points); 39 | this.drawLine(); 40 | this.eventDispatcher.dispatchEvent('drawUpdate', cartesian); 41 | } 42 | 43 | /** 44 | * In edit mode, drag key points to update corresponding key point data. 45 | */ 46 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 47 | this.points[index] = cartesian; 48 | this.setGeometryPoints(this.points); 49 | this.drawLine(); 50 | } 51 | 52 | getPoints() { 53 | return this.points; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/polygon/polygon.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class Polygon extends Base { 8 | points: Cartesian3[] = []; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.setState('drawing'); 14 | this.onDoubleClick(); 15 | } 16 | 17 | getType(): 'polygon' | 'line' { 18 | return 'polygon'; 19 | } 20 | 21 | /** 22 | * Add points only on click events 23 | */ 24 | addPoint(cartesian: Cartesian3) { 25 | this.points.push(cartesian); 26 | if (this.points.length === 1) { 27 | this.onMouseMove(); 28 | } 29 | } 30 | 31 | /** 32 | * Draw a shape based on mouse movement points during the initial drawing. 33 | */ 34 | updateMovingPoint(cartesian: Cartesian3) { 35 | const tempPoints = [...this.points, cartesian]; 36 | this.setGeometryPoints(tempPoints); 37 | if (tempPoints.length === 2) { 38 | this.addTempLine(); 39 | } else { 40 | this.removeTempLine(); 41 | this.drawPolygon(); 42 | } 43 | } 44 | 45 | /** 46 | * In edit mode, drag key points to update corresponding key point data. 47 | */ 48 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 49 | this.points[index] = cartesian; 50 | this.setGeometryPoints(this.points); 51 | this.drawPolygon(); 52 | } 53 | 54 | getPoints() { 55 | return this.points; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/polygon/triangle.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class Triangle extends Base { 8 | points: Cartesian3[] = []; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.setState('drawing'); 14 | } 15 | 16 | getType(): 'polygon' | 'line' { 17 | return 'polygon'; 18 | } 19 | 20 | /** 21 | * Add points only on click events 22 | */ 23 | addPoint(cartesian: Cartesian3) { 24 | this.points.push(cartesian); 25 | if (this.points.length === 1) { 26 | this.onMouseMove(); 27 | } else if (this.points.length === 3) { 28 | this.finishDrawing(); 29 | } 30 | } 31 | 32 | /** 33 | * Draw a shape based on mouse movement points during the initial drawing. 34 | */ 35 | updateMovingPoint(cartesian: Cartesian3) { 36 | const tempPoints = [...this.points, cartesian]; 37 | this.setGeometryPoints(tempPoints); 38 | if (tempPoints.length === 2) { 39 | this.addTempLine(); 40 | } else { 41 | this.removeTempLine(); 42 | this.drawPolygon(); 43 | } 44 | } 45 | 46 | /** 47 | * In edit mode, drag key points to update corresponding key point data. 48 | */ 49 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 50 | this.points[index] = cartesian; 51 | this.setGeometryPoints(this.points); 52 | this.drawPolygon(); 53 | } 54 | 55 | getPoints() { 56 | return this.points; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.6 2 | 3 | --- 4 | 5 | #### ✨ New Features 6 | 7 | - 新增: 绘制扇形 8 | 9 | - 新增: 根据数据回显图形 10 | 11 | - 新增: 获取图形关键点位方法:`getPoints` 12 | 13 | 14 | #### 🐞 Bug fixes 15 | 16 | - 修复:绘制过程中临时创建的线没有被删除的问题 17 | 18 | - 修复:双击控制点导致图形无法拖拽的问题 19 | 20 | - 修复:双箭头执行生长动画后,编辑状态无法拖拽的问题 21 | 22 | - 修复:双箭头整体被拖拽后生长动画路径不正确的问题 23 | 24 | 25 | ## 0.0.5 26 | 27 | --- 28 | 29 | #### ✨ New Features 30 | 31 | - Add: Fade-in Fade-out Animation for Graphics 32 | 33 | - Add: Growing Animation for Arrow-shaped Graphics 34 | 35 | #### ⚠️ Breaking changes 36 | 37 | - Set Default Style to Cesium Default Style 38 | 39 | #### 🐞 Bug fixes 40 | 41 | - Disable Cesium's Auto-Centering Viewport Behavior on Double Click 42 | 43 | 44 | ## 0.0.4 45 | 46 | --- 47 | 48 | #### ✨ New Features 49 | 50 | - Adding ES Module package to the published package 51 | 52 | #### 🐞 Bug fixes 53 | 54 | - Fix the issue where polygons and triangles cannot delete their outer edges when removed. 55 | 56 | ### 0.0.3 57 | 58 | --- 59 | 60 | #### ✨ New Features 61 | 62 | - Publish to CDN and NPM 63 | 64 | ## 0.0.2 65 | 66 | --- 67 | 68 | #### ✨ New Features 69 | 70 | - Adding graphical drawing capabilities: 71 | Triangle 72 | Polygon 73 | Circle 74 | 75 | ## 0.0.1 76 | 77 | --- 78 | 79 | #### ✨ New Features 80 | 81 | - Adding graphical drawing capabilities: 82 | FineArrow 83 | AttackArrow 84 | SwallowtailAttackArrow 85 | SquadCombat 86 | SwallowtailSquadCombat 87 | StraightArrow 88 | CurvedArrow 89 | AssaultDirection 90 | DoubleArrow 91 | FreehandLine 92 | FreehandPolygon 93 | Curve 94 | Ellipse 95 | Lune 96 | Reactangle 97 | -------------------------------------------------------------------------------- /src/polygon/freehand-polygon.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | import { PolygonStyle } from '../interface'; 5 | 6 | export default class FreehandPolygon extends Base { 7 | points: Cartesian3[] = []; 8 | freehand: boolean; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.freehand = true; 14 | this.setState('drawing'); 15 | } 16 | 17 | getType(): 'polygon' | 'line' { 18 | return 'polygon'; 19 | } 20 | 21 | /** 22 | * Add points only on click events 23 | */ 24 | addPoint(cartesian: Cartesian3) { 25 | this.points.push(cartesian); 26 | if (this.points.length === 1) { 27 | this.onMouseMove(); 28 | } else if (this.points.length > 2) { 29 | this.finishDrawing(); 30 | } 31 | } 32 | 33 | /** 34 | * Draw a shape based on mouse movement points during the initial drawing. 35 | */ 36 | updateMovingPoint(cartesian: Cartesian3) { 37 | this.points.push(cartesian); 38 | if (this.points.length > 2) { 39 | this.setGeometryPoints(this.points); 40 | this.drawPolygon(); 41 | this.eventDispatcher.dispatchEvent('drawUpdate', cartesian); 42 | } 43 | } 44 | 45 | /** 46 | * In edit mode, drag key points to update corresponding key point data. 47 | */ 48 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 49 | this.points[index] = cartesian; 50 | this.setGeometryPoints(this.points); 51 | this.drawPolygon(); 52 | } 53 | 54 | getPoints() { 55 | return this.points; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/polygon/rectangle.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class Rectangle extends Base { 8 | points: Cartesian3[] = []; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.setState('drawing'); 14 | } 15 | 16 | getType(): 'polygon' | 'line' { 17 | return 'polygon'; 18 | } 19 | 20 | /** 21 | * Add points only on click events 22 | */ 23 | addPoint(cartesian: Cartesian3) { 24 | this.points.push(cartesian); 25 | if (this.points.length === 1) { 26 | this.onMouseMove(); 27 | } else if (this.points.length > 1) { 28 | this.finishDrawing(); 29 | } 30 | } 31 | 32 | /** 33 | * Draw a shape based on mouse movement points during the initial drawing. 34 | */ 35 | updateMovingPoint(cartesian: Cartesian3) { 36 | const tempPoints = [...this.points, cartesian]; 37 | const geometryPoints = this.createGraphic(tempPoints); 38 | this.setGeometryPoints(geometryPoints); 39 | this.drawPolygon(); 40 | } 41 | 42 | /** 43 | * In edit mode, drag key points to update corresponding key point data. 44 | */ 45 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 46 | this.points[index] = cartesian; 47 | const geometryPoints = this.createGraphic(this.points); 48 | this.setGeometryPoints(geometryPoints); 49 | this.drawPolygon(); 50 | } 51 | 52 | createGraphic(positions: Cartesian3[]) { 53 | const [p1, p2] = positions.map(this.cartesianToLnglat); 54 | const coords = [...p1, p1[0], p2[1], ...p2, p2[0], p1[1], ...p1]; 55 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(coords); 56 | return cartesianPoints; 57 | } 58 | 59 | getPoints() { 60 | return this.points; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/arrow/assault-direction.ts: -------------------------------------------------------------------------------- 1 | import FineArrow from './fine-arrow'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class AssaultDirection extends FineArrow { 8 | points: Cartesian3[] = []; 9 | arrowLengthScale: number = 5; 10 | maxArrowLength: number = 2; 11 | tailWidthFactor: number; 12 | neckWidthFactor: number; 13 | headWidthFactor: number; 14 | headAngle: number; 15 | neckAngle: number; 16 | minPointsForShape: number; 17 | 18 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 19 | super(cesium, viewer, style); 20 | this.cesium = cesium; 21 | this.tailWidthFactor = 0.08; 22 | this.neckWidthFactor = 0.1; 23 | this.headWidthFactor = 0.13; 24 | this.headAngle = Math.PI / 4; 25 | this.neckAngle = Math.PI * 0.17741; 26 | this.minPointsForShape = 2; 27 | this.setState('drawing'); 28 | } 29 | 30 | createGraphic(positions: Cartesian3[]) { 31 | const [p1, p2] = positions.map(this.cartesianToLnglat); 32 | const len = Utils.getBaseLength([p1, p2]) * 1.5; 33 | const tailWidth = len * this.tailWidthFactor; 34 | const neckWidth = len * this.neckWidthFactor; 35 | const headWidth = len * this.headWidthFactor; 36 | const tailLeft = Utils.getThirdPoint(p2, p1, Math.PI / 2, tailWidth, true); 37 | const tailRight = Utils.getThirdPoint(p2, p1, Math.PI / 2, tailWidth, false); 38 | const headLeft = Utils.getThirdPoint(p1, p2, this.headAngle, headWidth, false); 39 | const headRight = Utils.getThirdPoint(p1, p2, this.headAngle, headWidth, true); 40 | const neckLeft = Utils.getThirdPoint(p1, p2, this.neckAngle, neckWidth, false); 41 | const neckRight = Utils.getThirdPoint(p1, p2, this.neckAngle, neckWidth, true); 42 | const points = [...tailLeft, ...neckLeft, ...headLeft, ...p2, ...headRight, ...neckRight, ...tailRight, ...p1]; 43 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(points); 44 | return cartesianPoints; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import FineArrow from './arrow/fine-arrow'; 2 | import AttackArrow from './arrow/attack-arrow'; 3 | import SwallowtailAttackArrow from './arrow/swallowtail-attack-arrow'; 4 | import SquadCombat from './arrow/squad-combat'; 5 | import SwallowtailSquadCombat from './arrow/swallowtail-squad-combat'; 6 | import StraightArrow from './arrow/straight-arrow'; 7 | import CurvedArrow from './arrow/curved-arrow'; 8 | import AssaultDirection from './arrow/assault-direction'; 9 | import DoubleArrow from './arrow/double-arrow'; 10 | import FreehandLine from './line/freehand-line'; 11 | import FreehandPolygon from './polygon/freehand-polygon'; 12 | import Curve from './line/curve'; 13 | import Ellipse from './polygon/ellipse'; 14 | import Lune from './polygon/lune'; 15 | import Reactangle from './polygon/rectangle'; 16 | import Triangle from './polygon/triangle'; 17 | import Polygon from './polygon/polygon'; 18 | import Circle from './polygon/circle'; 19 | import Sector from './polygon/sector'; 20 | 21 | import { GeometryStyle } from './interface'; 22 | import * as CesiumTypeOnly from 'cesium'; 23 | 24 | const CesiumPlot: any = { 25 | FineArrow, 26 | AttackArrow, 27 | SwallowtailAttackArrow, 28 | SquadCombat, 29 | SwallowtailSquadCombat, 30 | StraightArrow, 31 | CurvedArrow, 32 | AssaultDirection, 33 | DoubleArrow, 34 | FreehandLine, 35 | FreehandPolygon, 36 | Curve, 37 | Ellipse, 38 | Lune, 39 | Reactangle, 40 | Triangle, 41 | Polygon, 42 | Circle, 43 | Sector, 44 | }; 45 | 46 | type CreateGeometryFromDataOpts = { 47 | type: string; 48 | cartesianPoints: CesiumTypeOnly.Cartesian3[]; 49 | style: GeometryStyle; 50 | }; 51 | /** 52 | * 根据点位数据生成几何图形 53 | * @param points 54 | */ 55 | CesiumPlot.createGeometryFromData = (cesium: any, viewer: any, opts: CreateGeometryFromDataOpts) => { 56 | const { type, style, cartesianPoints } = opts; 57 | const geometry = new CesiumPlot[type](cesium, viewer, style); 58 | 59 | geometry.points = cartesianPoints; 60 | const geometryPoints = geometry.createGraphic(cartesianPoints); 61 | geometry.setGeometryPoints(geometryPoints); 62 | if (geometry.type == 'polygon') { 63 | geometry.drawPolygon(); 64 | } else { 65 | geometry.drawLine(); 66 | } 67 | geometry.finishDrawing(); 68 | geometry.onClick(); 69 | return geometry; 70 | }; 71 | 72 | export default CesiumPlot; 73 | -------------------------------------------------------------------------------- /src/line/curve.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import Base from '../base'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'kmap-3d-engine'; 5 | import { LineStyle } from '../interface'; 6 | 7 | export default class Curve extends Base { 8 | points: Cartesian3[] = []; 9 | arrowLengthScale: number = 5; 10 | maxArrowLength: number = 3000000; 11 | t: number; 12 | 13 | constructor(cesium: any, viewer: any, style?: LineStyle) { 14 | super(cesium, viewer, style); 15 | this.cesium = cesium; 16 | this.t = 0.3; 17 | this.setState('drawing'); 18 | this.onDoubleClick(); 19 | } 20 | 21 | getType(): 'polygon' | 'line' { 22 | return 'line'; 23 | } 24 | 25 | /** 26 | * Points are only added upon click events. 27 | */ 28 | addPoint(cartesian: Cartesian3) { 29 | this.points.push(cartesian); 30 | if (this.points.length < 2) { 31 | this.onMouseMove(); 32 | } else if (this.points.length === 2) { 33 | this.setGeometryPoints(this.points); 34 | this.drawLine(); 35 | } 36 | } 37 | 38 | /** 39 | * Draw the shape based on the mouse movement position during the initial drawing. 40 | */ 41 | updateMovingPoint(cartesian: Cartesian3) { 42 | const tempPoints = [...this.points, cartesian]; 43 | let geometryPoints = []; 44 | if (tempPoints.length === 2) { 45 | this.setGeometryPoints(tempPoints); 46 | this.drawLine(); 47 | } else { 48 | geometryPoints = this.createGraphic(tempPoints); 49 | this.setGeometryPoints(geometryPoints); 50 | } 51 | } 52 | 53 | /** 54 | * During editing mode, drag key points to update the corresponding data. 55 | */ 56 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 57 | this.points[index] = cartesian; 58 | const geometryPoints = this.createGraphic(this.points); 59 | this.setGeometryPoints(geometryPoints); 60 | this.drawLine(); 61 | } 62 | 63 | /** 64 | * Generate geometric shape points based on key points.. 65 | */ 66 | createGraphic(positions: Cartesian3[]) { 67 | const lnglatPoints = positions.map(pnt => { 68 | return this.cartesianToLnglat(pnt); 69 | }); 70 | 71 | const curvePoints = Utils.getCurvePoints(this.t, lnglatPoints); 72 | const temp = [].concat(...curvePoints); 73 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 74 | return cartesianPoints; 75 | } 76 | 77 | getPoints() { 78 | return this.points; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/polygon/sector.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | // @ts-ignore 3 | import { Cartesian3 } from 'cesium'; 4 | import * as Utils from '../utils'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class Sector extends Base { 8 | points: Cartesian3[] = []; 9 | 10 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 11 | super(cesium, viewer, style); 12 | this.cesium = cesium; 13 | this.setState('drawing'); 14 | } 15 | 16 | getType(): 'polygon' | 'line' { 17 | return 'polygon'; 18 | } 19 | 20 | /** 21 | * Add points only on click events 22 | */ 23 | addPoint(cartesian: Cartesian3) { 24 | this.points.push(cartesian); 25 | if (this.points.length === 1) { 26 | this.onMouseMove(); 27 | }else if (this.points.length === 3) { 28 | this.finishDrawing(); 29 | } 30 | } 31 | 32 | /** 33 | * Draw a shape based on mouse movement points during the initial drawing. 34 | */ 35 | updateMovingPoint(cartesian: Cartesian3) { 36 | const tempPoints = [...this.points, cartesian]; 37 | this.setGeometryPoints(tempPoints); 38 | if (tempPoints.length === 2) { 39 | this.addTempLine(); 40 | } else { 41 | this.removeTempLine(); 42 | const geometryPoints = this.createGraphic(tempPoints); 43 | this.setGeometryPoints(geometryPoints); 44 | this.drawPolygon(); 45 | } 46 | } 47 | 48 | createGraphic(positions: Cartesian3[]) { 49 | const lnglatPoints = positions.map((pnt) => { 50 | return this.cartesianToLnglat(pnt); 51 | }); 52 | const [center, pnt2, pnt3] = [lnglatPoints[0], lnglatPoints[1], lnglatPoints[2]]; 53 | const radius = Utils.MathDistance(pnt2, center); 54 | const startAngle = Utils.getAzimuth(pnt2, center); 55 | const endAngle = Utils.getAzimuth(pnt3, center); 56 | const res = Utils.getArcPoints(center, radius, startAngle, endAngle); 57 | res.push(center, res[0]); 58 | 59 | const temp = [].concat(...res); 60 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 61 | return cartesianPoints; 62 | } 63 | 64 | /** 65 | * In edit mode, drag key points to update corresponding key point data. 66 | */ 67 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 68 | this.points[index] = cartesian; 69 | const geometryPoints = this.createGraphic(this.points); 70 | this.setGeometryPoints(geometryPoints); 71 | this.drawPolygon(); 72 | } 73 | 74 | getPoints() { 75 | return this.points; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/polygon/circle.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | 6 | import { PolygonStyle } from '../interface'; 7 | 8 | export default class Circle extends Base { 9 | points: Cartesian3[] = []; 10 | freehand: boolean; 11 | 12 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 13 | super(cesium, viewer, style); 14 | this.cesium = cesium; 15 | this.freehand = true; 16 | this.setState('drawing'); 17 | } 18 | 19 | getType(): 'polygon' | 'line' { 20 | return 'polygon'; 21 | } 22 | 23 | /** 24 | * Add points only on click events 25 | */ 26 | addPoint(cartesian: Cartesian3) { 27 | this.points.push(cartesian); 28 | if (this.points.length === 1) { 29 | this.onMouseMove(); 30 | } else if (this.points.length > 1) { 31 | this.finishDrawing(); 32 | } 33 | } 34 | 35 | /** 36 | * Draw a shape based on mouse movement points during the initial drawing. 37 | */ 38 | updateMovingPoint(cartesian: Cartesian3) { 39 | const tempPoints = [...this.points, cartesian]; 40 | const geometryPoints = this.createGraphic(tempPoints); 41 | this.setGeometryPoints(geometryPoints); 42 | this.drawPolygon(); 43 | } 44 | 45 | /** 46 | * In edit mode, drag key points to update corresponding key point data. 47 | */ 48 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 49 | this.points[index] = cartesian; 50 | const geometryPoints = this.createGraphic(this.points); 51 | this.setGeometryPoints(geometryPoints); 52 | this.drawPolygon(); 53 | } 54 | 55 | createGraphic(positions: Cartesian3[]) { 56 | const lnglatPoints = positions.map((pnt) => { 57 | return this.cartesianToLnglat(pnt); 58 | }); 59 | const center = lnglatPoints[0]; 60 | const pnt2 = lnglatPoints[1]; 61 | 62 | const radius = Utils.MathDistance(center, pnt2); 63 | 64 | const res = this.generatePoints(center, radius); 65 | const temp = [].concat(...res); 66 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 67 | return cartesianPoints; 68 | } 69 | 70 | generatePoints(center, radius) { 71 | let x, y, angle; 72 | const points = []; 73 | for (let i = 0; i <= 100; i++) { 74 | angle = (Math.PI * 2 * i) / 100; 75 | x = center[0] + radius * Math.cos(angle); 76 | y = center[1] + radius * Math.sin(angle); 77 | points.push([x, y]); 78 | } 79 | return points; 80 | } 81 | 82 | getPoints() { 83 | return this.points; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/arrow/straight-arrow.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import Base from '../base'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { LineStyle } from '../interface'; 6 | 7 | export default class StraightArrow extends Base { 8 | points: Cartesian3[] = []; 9 | arrowLengthScale: number = 5; 10 | maxArrowLength: number = 3000000; 11 | minPointsForShape: number; 12 | 13 | constructor(cesium: any, viewer: any, style?: LineStyle) { 14 | super(cesium, viewer, style); 15 | this.cesium = cesium; 16 | this.minPointsForShape = 2; 17 | this.setState('drawing'); 18 | } 19 | 20 | getType(): 'polygon' | 'line' { 21 | return 'line'; 22 | } 23 | 24 | /** 25 | * Add points only on click events 26 | */ 27 | addPoint(cartesian: Cartesian3) { 28 | if (this.points.length < 2) { 29 | this.points.push(cartesian); 30 | this.onMouseMove(); 31 | } 32 | if (this.points.length === 2) { 33 | const geometryPoints = this.createGraphic(this.points); 34 | this.setGeometryPoints(geometryPoints); 35 | this.drawLine(); 36 | this.finishDrawing(); 37 | } 38 | } 39 | 40 | /** 41 | * Draw a shape based on mouse movement points during the initial drawing. 42 | */ 43 | updateMovingPoint(cartesian: Cartesian3) { 44 | const tempPoints = [...this.points, cartesian]; 45 | const geometryPoints = this.createGraphic(tempPoints); 46 | this.setGeometryPoints(geometryPoints); 47 | this.drawLine(); 48 | } 49 | 50 | /** 51 | * In edit mode, drag key points to update corresponding key point data. 52 | */ 53 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 54 | this.points[index] = cartesian; 55 | const geometryPoints = this.createGraphic(this.points); 56 | this.setGeometryPoints(geometryPoints); 57 | this.drawLine(); 58 | } 59 | 60 | /** 61 | * Generate geometric shapes based on key points. 62 | */ 63 | createGraphic(positions: Cartesian3[]) { 64 | const [pnt1, pnt2] = positions.map(this.cartesianToLnglat); 65 | const distance = Utils.MathDistance(pnt1, pnt2); 66 | let len = distance / this.arrowLengthScale; 67 | len = len > this.maxArrowLength ? this.maxArrowLength : len; 68 | const leftPnt = Utils.getThirdPoint(pnt1, pnt2, Math.PI / 6, len / 2, false); 69 | const rightPnt = Utils.getThirdPoint(pnt1, pnt2, Math.PI / 6, len / 2, true); 70 | const points = [...pnt1, ...pnt2, ...leftPnt, ...pnt2, ...rightPnt]; 71 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(points); 72 | return cartesianPoints; 73 | } 74 | 75 | getPoints() { 76 | return this.points; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/arrow/swallowtail-squad-combat.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import SquadCombat from './squad-combat'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class SwallowtailSquadCombat extends SquadCombat { 8 | points: Cartesian3[] = []; 9 | headHeightFactor: number; 10 | headWidthFactor: number; 11 | neckHeightFactor: number; 12 | neckWidthFactor: number; 13 | tailWidthFactor: number; 14 | swallowTailFactor: number; 15 | 16 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 17 | super(cesium, viewer, style); 18 | 19 | this.cesium = cesium; 20 | this.headHeightFactor = 0.18; 21 | this.headWidthFactor = 0.3; 22 | this.neckHeightFactor = 0.85; 23 | this.neckWidthFactor = 0.15; 24 | this.tailWidthFactor = 0.1; 25 | this.swallowTailFactor = 1; 26 | this.minPointsForShape = 2; 27 | } 28 | 29 | /** 30 | * Generate geometric shapes based on key points. 31 | */ 32 | createGraphic(positions: Cartesian3[]): Cartesian3[] { 33 | const lnglatPoints = positions.map((pnt) => { 34 | return this.cartesianToLnglat(pnt); 35 | }); 36 | 37 | const tailPnts = this.getTailPoints(lnglatPoints); 38 | const headPnts = this.getArrowHeadPoints(lnglatPoints, tailPnts[0], tailPnts[2]); 39 | const neckLeft = headPnts[0]; 40 | const neckRight = headPnts[4]; 41 | const bodyPnts = this.getArrowBodyPoints(lnglatPoints, neckLeft, neckRight, this.tailWidthFactor); 42 | const count = bodyPnts.length; 43 | let leftPnts = [tailPnts[0]].concat(bodyPnts.slice(0, count / 2)); 44 | leftPnts.push(neckLeft); 45 | let rightPnts = [tailPnts[2]].concat(bodyPnts.slice(count / 2, count)); 46 | rightPnts.push(neckRight); 47 | leftPnts = Utils.getQBSplinePoints(leftPnts); 48 | rightPnts = Utils.getQBSplinePoints(rightPnts); 49 | 50 | const points = leftPnts.concat(headPnts, rightPnts.reverse(), [tailPnts[1], leftPnts[0]]); 51 | const temp = [].concat(...points); 52 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 53 | return cartesianPoints; 54 | } 55 | 56 | getTailPoints(points) { 57 | const allLen = Utils.getBaseLength(points); 58 | const tailWidth = allLen * this.tailWidthFactor; 59 | const tailLeft = Utils.getThirdPoint(points[1], points[0], Math.PI / 2, tailWidth, false); 60 | const tailRight = Utils.getThirdPoint(points[1], points[0], Math.PI / 2, tailWidth, true); 61 | const len = tailWidth * this.swallowTailFactor; 62 | const swallowTailPnt = Utils.getThirdPoint(points[1], points[0], 0, len, true); 63 | return [tailLeft, swallowTailPnt, tailRight]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CesiumPlot 8 | 9 | 10 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/polygon/ellipse.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | 6 | import { PolygonStyle } from '../interface'; 7 | 8 | export default class Ellipse extends Base { 9 | points: Cartesian3[] = []; 10 | freehand: boolean; 11 | 12 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 13 | super(cesium, viewer, style); 14 | this.cesium = cesium; 15 | this.freehand = true; 16 | this.setState('drawing'); 17 | } 18 | 19 | getType(): 'polygon' | 'line' { 20 | return 'polygon'; 21 | } 22 | 23 | /** 24 | * Add points only on click events 25 | */ 26 | addPoint(cartesian: Cartesian3) { 27 | this.points.push(cartesian); 28 | if (this.points.length === 1) { 29 | this.onMouseMove(); 30 | } else if (this.points.length > 1) { 31 | this.finishDrawing(); 32 | } 33 | } 34 | 35 | /** 36 | * Draw a shape based on mouse movement points during the initial drawing. 37 | */ 38 | updateMovingPoint(cartesian: Cartesian3) { 39 | const tempPoints = [...this.points, cartesian]; 40 | const geometryPoints = this.createGraphic(tempPoints); 41 | this.setGeometryPoints(geometryPoints); 42 | this.drawPolygon(); 43 | } 44 | 45 | /** 46 | * In edit mode, drag key points to update corresponding key point data. 47 | */ 48 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 49 | this.points[index] = cartesian; 50 | const geometryPoints = this.createGraphic(this.points); 51 | this.setGeometryPoints(geometryPoints); 52 | this.drawPolygon(); 53 | } 54 | 55 | createGraphic(positions: Cartesian3[]) { 56 | const lnglatPoints = positions.map((pnt) => { 57 | return this.cartesianToLnglat(pnt); 58 | }); 59 | const pnt1 = lnglatPoints[0]; 60 | const pnt2 = lnglatPoints[1]; 61 | 62 | const center = Utils.Mid(pnt1, pnt2); 63 | const majorRadius = Math.abs((pnt1[0] - pnt2[0]) / 2); 64 | const minorRadius = Math.abs((pnt1[1] - pnt2[1]) / 2); 65 | const res = this.generatePoints(center, majorRadius, minorRadius); 66 | const temp = [].concat(...res); 67 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 68 | return cartesianPoints; 69 | } 70 | 71 | generatePoints(center, majorRadius, minorRadius) { 72 | let [x, y, angle, points] = [null, null, 0, []]; 73 | for (let i = 0; i <= 100; i++) { 74 | angle = (Math.PI * 2 * i) / 100; 75 | x = center[0] + majorRadius * Math.cos(angle); 76 | y = center[1] + minorRadius * Math.sin(angle); 77 | points.push([x, y]); 78 | } 79 | return points; 80 | } 81 | 82 | getPoints() { 83 | return this.points; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/arrow/swallowtail-attack-arrow.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import AttackArrow from './attack-arrow'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class SwallowtailAttackArrow extends AttackArrow { 8 | points: Cartesian3[] = []; 9 | headHeightFactor: number; 10 | headWidthFactor: number; 11 | neckHeightFactor: number; 12 | neckWidthFactor: number; 13 | headTailFactor: number; 14 | tailWidthFactor: number; 15 | swallowTailFactor: number; 16 | swallowTailPnt: [number, number]; 17 | 18 | constructor(cesium: any, viewer: any, style: PolygonStyle) { 19 | super(cesium, viewer, style); 20 | this.cesium = cesium; 21 | this.headHeightFactor = 0.18; 22 | this.headWidthFactor = 0.3; 23 | this.neckHeightFactor = 0.85; 24 | this.neckWidthFactor = 0.15; 25 | this.tailWidthFactor = 0.1; 26 | this.headTailFactor = 0.8; 27 | this.swallowTailFactor = 1; 28 | this.swallowTailPnt = [0, 0]; 29 | this.minPointsForShape = 3; 30 | } 31 | 32 | /** 33 | * Generate geometric shapes based on key points. 34 | */ 35 | createGraphic(positions: Cartesian3[]): Cartesian3[] { 36 | const lnglatPoints = positions.map((pnt) => { 37 | return this.cartesianToLnglat(pnt); 38 | }); 39 | let [tailLeft, tailRight] = [lnglatPoints[0], lnglatPoints[1]]; 40 | if (Utils.isClockWise(lnglatPoints[0], lnglatPoints[1], lnglatPoints[2])) { 41 | tailLeft = lnglatPoints[1]; 42 | tailRight = lnglatPoints[0]; 43 | } 44 | const midTail = Utils.Mid(tailLeft, tailRight); 45 | const bonePnts = [midTail].concat(lnglatPoints.slice(2)); 46 | const headPnts = this.getArrowHeadPoints(bonePnts, tailLeft, tailRight); 47 | const [neckLeft, neckRight] = [headPnts[0], headPnts[4]]; 48 | const tailWidth = Utils.MathDistance(tailLeft, tailRight); 49 | const allLen = Utils.getBaseLength(bonePnts); 50 | const len = allLen * this.tailWidthFactor * this.swallowTailFactor; 51 | this.swallowTailPnt = Utils.getThirdPoint(bonePnts[1], bonePnts[0], 0, len, true); 52 | const factor = tailWidth / allLen; 53 | const bodyPnts = this.getArrowBodyPoints(bonePnts, neckLeft, neckRight, factor); 54 | const count = bodyPnts.length; 55 | let leftPnts = [tailLeft].concat(bodyPnts.slice(0, count / 2)); 56 | leftPnts.push(neckLeft); 57 | let rightPnts = [tailRight].concat(bodyPnts.slice(count / 2, count)); 58 | rightPnts.push(neckRight); 59 | leftPnts = Utils.getQBSplinePoints(leftPnts); 60 | rightPnts = Utils.getQBSplinePoints(rightPnts); 61 | const points = leftPnts.concat(headPnts, rightPnts.reverse(), [this.swallowTailPnt, leftPnts[0]]); 62 | const temp = [].concat(...points); 63 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 64 | return cartesianPoints; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CesiumPlot 7 | 43 | 44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/polygon/lune.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class Lune extends Base { 8 | points: Cartesian3[] = []; 9 | freehand: boolean; 10 | 11 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 12 | super(cesium, viewer, style); 13 | this.cesium = cesium; 14 | this.freehand = true; 15 | this.setState('drawing'); 16 | } 17 | 18 | getType(): 'polygon' | 'line' { 19 | return 'polygon'; 20 | } 21 | 22 | /** 23 | * Add points only on click events 24 | */ 25 | addPoint(cartesian: Cartesian3) { 26 | this.points.push(cartesian); 27 | if (this.points.length === 1) { 28 | this.onMouseMove(); 29 | } else if (this.points.length === 2) { 30 | } else if (this.points.length > 2) { 31 | this.finishDrawing(); 32 | } 33 | } 34 | 35 | /** 36 | * Draw a shape based on mouse movement points during the initial drawing. 37 | */ 38 | updateMovingPoint(cartesian: Cartesian3) { 39 | const tempPoints = [...this.points, cartesian]; 40 | const geometryPoints = this.createGraphic(tempPoints); 41 | this.setGeometryPoints(geometryPoints); 42 | this.drawPolygon(); 43 | } 44 | 45 | /** 46 | * In edit mode, drag key points to update corresponding key point data. 47 | */ 48 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 49 | this.points[index] = cartesian; 50 | const geometryPoints = this.createGraphic(this.points); 51 | this.setGeometryPoints(geometryPoints); 52 | this.drawPolygon(); 53 | } 54 | 55 | createGraphic(positions: Cartesian3[]) { 56 | const lnglatPoints = positions.map((pnt) => { 57 | return this.cartesianToLnglat(pnt); 58 | }); 59 | 60 | if (lnglatPoints.length === 2) { 61 | const mid = Utils.Mid(lnglatPoints[0], lnglatPoints[1]); 62 | const d = Utils.MathDistance(lnglatPoints[0], mid); 63 | const pnt = Utils.getThirdPoint(lnglatPoints[0], mid, Math.PI / 2, d, false); 64 | lnglatPoints.push(pnt); 65 | } 66 | let [pnt1, pnt2, pnt3, startAngle, endAngle] = [ 67 | lnglatPoints[0], 68 | lnglatPoints[1], 69 | lnglatPoints[2], 70 | undefined, 71 | undefined, 72 | ]; 73 | const center = Utils.getCircleCenterOfThreePoints(pnt1, pnt2, pnt3); 74 | const radius = Utils.MathDistance(pnt1, center); 75 | const angle1 = Utils.getAzimuth(pnt1, center); 76 | const angle2 = Utils.getAzimuth(pnt2, center); 77 | if (Utils.isClockWise(pnt1, pnt2, pnt3)) { 78 | startAngle = angle2; 79 | endAngle = angle1; 80 | } else { 81 | startAngle = angle1; 82 | endAngle = angle2; 83 | } 84 | 85 | let points = Utils.getArcPoints(center, radius, startAngle, endAngle); 86 | const temp = [].concat(...points); 87 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 88 | return cartesianPoints; 89 | } 90 | 91 | getPoints() { 92 | return this.points; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/arrow/squad-combat.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import AttackArrow from './attack-arrow'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class SquadCombat extends AttackArrow { 8 | points: Cartesian3[] = []; 9 | headHeightFactor: number; 10 | headWidthFactor: number; 11 | neckHeightFactor: number; 12 | neckWidthFactor: number; 13 | tailWidthFactor: number; 14 | 15 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 16 | super(cesium, viewer, style); 17 | this.cesium = cesium; 18 | this.headHeightFactor = 0.18; 19 | this.headWidthFactor = 0.3; 20 | this.neckHeightFactor = 0.85; 21 | this.neckWidthFactor = 0.15; 22 | this.tailWidthFactor = 0.1; 23 | this.minPointsForShape = 2; 24 | } 25 | 26 | /** 27 | * Add points only on click events 28 | */ 29 | addPoint(cartesian: Cartesian3) { 30 | this.points.push(cartesian); 31 | if (this.points.length < 2) { 32 | this.onMouseMove(); 33 | } else if (this.points.length > 2) { 34 | this.lineEntity && this.viewer.entities.remove(this.lineEntity); 35 | } 36 | } 37 | 38 | /** 39 | * Draw a shape based on mouse movement points during the initial drawing. 40 | */ 41 | updateMovingPoint(cartesian: Cartesian3) { 42 | const tempPoints = [...this.points, cartesian]; 43 | this.setGeometryPoints(tempPoints); 44 | if (tempPoints.length < 2) { 45 | return; 46 | } else { 47 | const geometryPoints = this.createGraphic(tempPoints); 48 | this.setGeometryPoints(geometryPoints); 49 | this.drawPolygon(); 50 | } 51 | } 52 | 53 | /** 54 | * Generate geometric shapes based on key points. 55 | */ 56 | createGraphic(positions: Cartesian3[]): Cartesian3[] { 57 | const lnglatPoints = positions.map((pnt) => { 58 | return this.cartesianToLnglat(pnt); 59 | }); 60 | const tailPnts = this.getTailPoints(lnglatPoints); 61 | const headPnts = this.getArrowHeadPoints(lnglatPoints, tailPnts[0], tailPnts[1]); 62 | const neckLeft = headPnts[0]; 63 | const neckRight = headPnts[4]; 64 | const bodyPnts = this.getArrowBodyPoints(lnglatPoints, neckLeft, neckRight, this.tailWidthFactor); 65 | const count = bodyPnts.length; 66 | let leftPnts = [tailPnts[0]].concat(bodyPnts.slice(0, count / 2)); 67 | leftPnts.push(neckLeft); 68 | let rightPnts = [tailPnts[1]].concat(bodyPnts.slice(count / 2, count)); 69 | rightPnts.push(neckRight); 70 | leftPnts = Utils.getQBSplinePoints(leftPnts); 71 | rightPnts = Utils.getQBSplinePoints(rightPnts); 72 | const points = leftPnts.concat(headPnts, rightPnts.reverse()); 73 | const temp = [].concat(...points); 74 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 75 | return cartesianPoints; 76 | } 77 | 78 | getTailPoints(points) { 79 | const allLen = Utils.getBaseLength(points); 80 | const tailWidth = allLen * this.tailWidthFactor; 81 | const tailLeft = Utils.getThirdPoint(points[1], points[0], Math.PI / 2, tailWidth, false); 82 | const tailRight = Utils.getThirdPoint(points[1], points[0], Math.PI / 2, tailWidth, true); 83 | return [tailLeft, tailRight]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/arrow/fine-arrow.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class FineArrow extends Base { 8 | points: Cartesian3[] = []; 9 | arrowLengthScale: number = 5; 10 | maxArrowLength: number = 2; 11 | tailWidthFactor: number; 12 | neckWidthFactor: number; 13 | headWidthFactor: number; 14 | headAngle: number; 15 | neckAngle: number; 16 | minPointsForShape: number; 17 | 18 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 19 | super(cesium, viewer, style); 20 | this.cesium = cesium; 21 | this.tailWidthFactor = 0.1; 22 | this.neckWidthFactor = 0.2; 23 | this.headWidthFactor = 0.25; 24 | this.headAngle = Math.PI / 8.5; 25 | this.neckAngle = Math.PI / 13; 26 | this.minPointsForShape = 2; 27 | this.setState('drawing'); 28 | } 29 | 30 | getType(): 'polygon' | 'line' { 31 | return 'polygon'; 32 | } 33 | 34 | /** 35 | * Add points only on click events 36 | */ 37 | addPoint(cartesian: Cartesian3) { 38 | if (this.points.length < 2) { 39 | this.points.push(cartesian); 40 | this.onMouseMove(); 41 | } 42 | if (this.points.length === 2) { 43 | const geometryPoints = this.createGraphic(this.points); 44 | this.setGeometryPoints(geometryPoints); 45 | this.drawPolygon(); 46 | this.finishDrawing(); 47 | } 48 | } 49 | 50 | /** 51 | * Draw a shape based on mouse movement points during the initial drawing. 52 | */ 53 | updateMovingPoint(cartesian: Cartesian3) { 54 | const tempPoints = [...this.points, cartesian]; 55 | const geometryPoints = this.createGraphic(tempPoints); 56 | this.setGeometryPoints(geometryPoints); 57 | this.drawPolygon(); 58 | } 59 | 60 | /** 61 | * In edit mode, drag key points to update corresponding key point data. 62 | */ 63 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 64 | this.points[index] = cartesian; 65 | const geometryPoints = this.createGraphic(this.points); 66 | this.setGeometryPoints(geometryPoints); 67 | this.drawPolygon(); 68 | } 69 | 70 | /** 71 | * Generate geometric shapes based on key points. 72 | */ 73 | createGraphic(positions: Cartesian3[]) { 74 | const [p1, p2] = positions.map(this.cartesianToLnglat); 75 | const len = Utils.getBaseLength([p1, p2]); 76 | const tailWidth = len * this.tailWidthFactor; 77 | const neckWidth = len * this.neckWidthFactor; 78 | const headWidth = len * this.headWidthFactor; 79 | const tailLeft = Utils.getThirdPoint(p2, p1, Math.PI / 2, tailWidth, true); 80 | const tailRight = Utils.getThirdPoint(p2, p1, Math.PI / 2, tailWidth, false); 81 | const headLeft = Utils.getThirdPoint(p1, p2, this.headAngle, headWidth, false); 82 | const headRight = Utils.getThirdPoint(p1, p2, this.headAngle, headWidth, true); 83 | const neckLeft = Utils.getThirdPoint(p1, p2, this.neckAngle, neckWidth, false); 84 | const neckRight = Utils.getThirdPoint(p1, p2, this.neckAngle, neckWidth, true); 85 | const points = [...tailLeft, ...neckLeft, ...headLeft, ...p2, ...headRight, ...neckRight, ...tailRight, ...p1]; 86 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(points); 87 | return cartesianPoints; 88 | } 89 | 90 | getPoints() { 91 | return this.points; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/arrow/curved-arrow.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../utils'; 2 | import Base from '../base'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { LineStyle } from '../interface'; 6 | 7 | export default class CurvedArrow extends Base { 8 | points: Cartesian3[] = []; 9 | arrowLengthScale: number = 5; 10 | maxArrowLength: number = 3000000; 11 | t: number; 12 | minPointsForShape: number; 13 | 14 | constructor(cesium: any, viewer: any, style?: LineStyle) { 15 | super(cesium, viewer, style); 16 | this.cesium = cesium; 17 | this.t = 0.3; 18 | this.minPointsForShape = 2; 19 | this.setState('drawing'); 20 | this.onDoubleClick(); 21 | } 22 | 23 | getType(): 'polygon' | 'line' { 24 | return 'line'; 25 | } 26 | 27 | /** 28 | * Add points only on click events 29 | */ 30 | addPoint(cartesian: Cartesian3) { 31 | this.points.push(cartesian); 32 | if (this.points.length < 2) { 33 | this.onMouseMove(); 34 | } 35 | } 36 | 37 | /** 38 | * Draw a shape based on mouse movement points during the initial drawing. 39 | */ 40 | updateMovingPoint(cartesian: Cartesian3) { 41 | const tempPoints = [...this.points, cartesian]; 42 | let geometryPoints = this.createGraphic(tempPoints); 43 | this.setGeometryPoints(geometryPoints); 44 | this.drawLine(); 45 | } 46 | 47 | createStraightArrow(positions: Cartesian3[]) { 48 | const [pnt1, pnt2] = positions.map(this.cartesianToLnglat); 49 | const distance = Utils.MathDistance(pnt1, pnt2); 50 | let len = distance / this.arrowLengthScale; 51 | len = len > this.maxArrowLength ? this.maxArrowLength : len; 52 | const leftPnt = Utils.getThirdPoint(pnt1, pnt2, Math.PI / 6, len / 2, false); 53 | const rightPnt = Utils.getThirdPoint(pnt1, pnt2, Math.PI / 6, len / 2, true); 54 | const points = [...pnt1, ...pnt2, ...leftPnt, ...pnt2, ...rightPnt]; 55 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(points); 56 | return cartesianPoints; 57 | } 58 | 59 | /** 60 | * In edit mode, drag key points to update corresponding key point data. 61 | */ 62 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 63 | this.points[index] = cartesian; 64 | const geometryPoints = this.createGraphic(this.points); 65 | this.setGeometryPoints(geometryPoints); 66 | this.drawLine(); 67 | } 68 | 69 | /** 70 | * Generate geometric shapes based on key points. 71 | */ 72 | createGraphic(positions: Cartesian3[]) { 73 | const lnglatPoints = positions.map((pnt) => { 74 | return this.cartesianToLnglat(pnt); 75 | }); 76 | 77 | if (positions.length === 2) { 78 | // If there are only two points, draw a fine straight arrow. 79 | return this.createStraightArrow(positions); 80 | } 81 | 82 | const curvePoints = Utils.getCurvePoints(this.t, lnglatPoints); 83 | const pnt1 = lnglatPoints[lnglatPoints.length - 2]; 84 | const pnt2 = lnglatPoints[lnglatPoints.length - 1]; 85 | 86 | const distance = Utils.wholeDistance(lnglatPoints); 87 | let len = distance / this.arrowLengthScale; 88 | len = len > this.maxArrowLength ? this.maxArrowLength : len; 89 | const leftPnt = Utils.getThirdPoint(curvePoints[curvePoints.length - 2], curvePoints[curvePoints.length - 1], Math.PI / 6, len / 2, false); 90 | const rightPnt = Utils.getThirdPoint(curvePoints[curvePoints.length - 2], curvePoints[curvePoints.length - 1], Math.PI / 6, len / 2, true); 91 | const temp = [].concat(...curvePoints); 92 | const points = [...temp, ...leftPnt, ...pnt2, ...rightPnt]; 93 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(points); 94 | return cartesianPoints; 95 | } 96 | 97 | getPoints() { 98 | return this.points; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /debug/index.ts: -------------------------------------------------------------------------------- 1 | import CesiumPlot from '../src'; 2 | import * as Cesium from 'cesium'; 3 | import 'cesium/Build/Cesium/Widgets/widgets.css'; 4 | let raster = new Cesium.ArcGisMapServerImageryProvider({ 5 | url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer', 6 | }); 7 | 8 | const viewer = new Cesium.Viewer('cesiumContainer', { 9 | animation: false, 10 | shouldAnimate: true, 11 | geocoder: false, 12 | homeButton: false, 13 | infoBox: false, 14 | fullscreenButton: false, 15 | sceneModePicker: false, 16 | selectionIndicator: false, 17 | timeline: false, 18 | navigationHelpButton: false, 19 | baseLayerPicker: false, 20 | imageryProvider: raster, 21 | contextOptions: { 22 | requestWebgl2: true, 23 | }, 24 | }); 25 | 26 | viewer.scene.postProcessStages.fxaa.enabled = true; 27 | viewer.scene.camera.setView({ 28 | destination: Cesium.Cartesian3.fromDegrees(107.857, 35.594498, 10000), 29 | }); 30 | 31 | let geometry: any; 32 | let geometryType: string; 33 | const dragStartHandler = () => { 34 | console.error('start'); 35 | }; 36 | const drawUpdateHandler = (cartesian: Cesium.Cartesian3) => { 37 | console.error('update', cartesian); 38 | }; 39 | 40 | const drawEndHandler = (geometryPoints: any) => { 41 | console.error('drawEnd', geometryPoints); 42 | }; 43 | 44 | const editStartHandler = () => { 45 | console.error('editStart'); 46 | }; 47 | 48 | const editEndHandler = (geometryPoints: any) => { 49 | console.error('editEnd', geometryPoints); 50 | }; 51 | 52 | const buttonGroup = document.getElementById('button-group') as HTMLElement; 53 | buttonGroup.onclick = (evt) => { 54 | const targetElement = evt.target as HTMLElement; 55 | geometryType = targetElement.id; 56 | geometry = new CesiumPlot[geometryType](Cesium, viewer, { 57 | material: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 0.5)'), 58 | outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 59 | outlineWidth: 3, 60 | }); 61 | }; 62 | 63 | const operationButtonGroup = document.getElementById('operation-button-group') as HTMLElement; 64 | operationButtonGroup.onclick = (evt) => { 65 | const targetElement = evt.target as HTMLElement; 66 | switch (targetElement.id) { 67 | case 'hide': 68 | geometry && 69 | geometry.hide({ 70 | duration: 1000, 71 | delay: 0, 72 | }); 73 | break; 74 | case 'show': 75 | geometry && geometry.show(); 76 | break; 77 | case 'remove': 78 | geometry && geometry.remove(); 79 | // geometry = null; 80 | break; 81 | case 'addEvent': 82 | if (geometry) { 83 | geometry.on('drawStart', dragStartHandler); 84 | geometry.on('drawUpdate', drawUpdateHandler); 85 | geometry.on('drawEnd', drawEndHandler); 86 | geometry.on('editStart', editStartHandler); 87 | geometry.on('editEnd', editEndHandler); 88 | } 89 | break; 90 | case 'removeEvent': 91 | if (geometry) { 92 | geometry.off('drawStart', dragStartHandler); 93 | geometry.off('drawUpdate', drawUpdateHandler); 94 | geometry.off('drawEnd', drawEndHandler); 95 | geometry.off('editStart', editStartHandler); 96 | geometry.off('editEnd', editEndHandler); 97 | } 98 | break; 99 | case 'startGrowthAnimation': 100 | if (geometry) { 101 | geometry.startGrowthAnimation(); 102 | } 103 | break; 104 | case 'createGeometryFromData': 105 | if (geometry) { 106 | const points = geometry.getPoints(); 107 | geometry = CesiumPlot.createGeometryFromData(Cesium, viewer, { 108 | type: geometryType, 109 | cartesianPoints: points, 110 | style: { 111 | material: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 0.5)'), 112 | outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 113 | outlineWidth: 3, 114 | }, 115 | }); 116 | } 117 | break; 118 | case 'cancelDraw': 119 | if (geometry) { 120 | geometry.remove(); 121 | } 122 | break; 123 | default: 124 | break; 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /src/arrow/attack-arrow.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | 7 | export default class AttackArrow extends Base { 8 | points: Cartesian3[] = []; 9 | headHeightFactor: number; 10 | headWidthFactor: number; 11 | neckHeightFactor: number; 12 | neckWidthFactor: number; 13 | headTailFactor: number; 14 | minPointsForShape: number; 15 | 16 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 17 | super(cesium, viewer, style); 18 | this.cesium = cesium; 19 | this.headHeightFactor = 0.18; 20 | this.headWidthFactor = 0.3; 21 | this.neckHeightFactor = 0.85; 22 | this.neckWidthFactor = 0.15; 23 | this.headTailFactor = 0.8; 24 | this.minPointsForShape = 3; 25 | this.setState('drawing'); 26 | this.onDoubleClick(); 27 | } 28 | 29 | getType(): 'polygon' | 'line' { 30 | return 'polygon'; 31 | } 32 | 33 | /** 34 | * Add points only on click events 35 | */ 36 | addPoint(cartesian: Cartesian3) { 37 | this.points.push(cartesian); 38 | if (this.points.length < 2) { 39 | this.onMouseMove(); 40 | } else if (this.points.length === 2) { 41 | this.setGeometryPoints(this.points); 42 | this.drawPolygon(); 43 | } 44 | } 45 | 46 | /** 47 | * Draw a shape based on mouse movement points during the initial drawing. 48 | */ 49 | updateMovingPoint(cartesian: Cartesian3) { 50 | const tempPoints = [...this.points, cartesian]; 51 | this.setGeometryPoints(tempPoints); 52 | if (tempPoints.length === 2) { 53 | this.addTempLine(); 54 | } else { 55 | this.removeTempLine(); 56 | const geometryPoints = this.createGraphic(tempPoints); 57 | this.setGeometryPoints(geometryPoints); 58 | this.drawPolygon(); 59 | } 60 | } 61 | 62 | /** 63 | * Generate geometric shapes based on key points. 64 | */ 65 | createGraphic(positions: Cartesian3[]): Cartesian3[] { 66 | const lnglatPoints = positions.map((pnt) => { 67 | return this.cartesianToLnglat(pnt); 68 | }); 69 | 70 | let [tailLeft, tailRight] = [lnglatPoints[0], lnglatPoints[1]]; 71 | if (Utils.isClockWise(lnglatPoints[0], lnglatPoints[1], lnglatPoints[2])) { 72 | tailLeft = lnglatPoints[1]; 73 | tailRight = lnglatPoints[0]; 74 | } 75 | 76 | const midTail = Utils.Mid(tailLeft, tailRight); 77 | const bonePnts = [midTail].concat(lnglatPoints.slice(2)); 78 | const headPnts = this.getArrowHeadPoints(bonePnts, tailLeft, tailRight); 79 | const [neckLeft, neckRight] = [headPnts[0], headPnts[4]]; 80 | const tailWidthFactor = Utils.MathDistance(tailLeft, tailRight) / Utils.getBaseLength(bonePnts); 81 | const bodyPnts = this.getArrowBodyPoints(bonePnts, neckLeft, neckRight, tailWidthFactor); 82 | const count = bodyPnts.length; 83 | let leftPnts = [tailLeft].concat(bodyPnts.slice(0, count / 2)); 84 | leftPnts.push(neckLeft); 85 | let rightPnts = [tailRight].concat(bodyPnts.slice(count / 2, count)); 86 | rightPnts.push(neckRight); 87 | leftPnts = Utils.getQBSplinePoints(leftPnts); 88 | rightPnts = Utils.getQBSplinePoints(rightPnts); 89 | const points = leftPnts.concat(headPnts, rightPnts.reverse()); 90 | const temp = [].concat(...points); 91 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 92 | return cartesianPoints; 93 | } 94 | 95 | getPoints() { 96 | return this.points; 97 | } 98 | 99 | getArrowHeadPoints(points, tailLeft, tailRight) { 100 | try { 101 | let len = Utils.getBaseLength(points); 102 | let headHeight = len * this.headHeightFactor; 103 | const headPnt = points[points.length - 1]; 104 | len = Utils.MathDistance(headPnt, points[points.length - 2]); 105 | const tailWidth = Utils.MathDistance(tailLeft, tailRight); 106 | if (headHeight > tailWidth * this.headTailFactor) { 107 | headHeight = tailWidth * this.headTailFactor; 108 | } 109 | const headWidth = headHeight * this.headWidthFactor; 110 | const neckWidth = headHeight * this.neckWidthFactor; 111 | headHeight = headHeight > len ? len : headHeight; 112 | const neckHeight = headHeight * this.neckHeightFactor; 113 | const headEndPnt = Utils.getThirdPoint(points[points.length - 2], headPnt, 0, headHeight, true); 114 | const neckEndPnt = Utils.getThirdPoint(points[points.length - 2], headPnt, 0, neckHeight, true); 115 | const headLeft = Utils.getThirdPoint(headPnt, headEndPnt, Math.PI / 2, headWidth, false); 116 | const headRight = Utils.getThirdPoint(headPnt, headEndPnt, Math.PI / 2, headWidth, true); 117 | const neckLeft = Utils.getThirdPoint(headPnt, neckEndPnt, Math.PI / 2, neckWidth, false); 118 | const neckRight = Utils.getThirdPoint(headPnt, neckEndPnt, Math.PI / 2, neckWidth, true); 119 | return [neckLeft, headLeft, headPnt, headRight, neckRight]; 120 | } catch (e) { 121 | console.log(e); 122 | } 123 | } 124 | 125 | getArrowBodyPoints(points, neckLeft, neckRight, tailWidthFactor) { 126 | const allLen = Utils.wholeDistance(points); 127 | const len = Utils.getBaseLength(points); 128 | const tailWidth = len * tailWidthFactor; 129 | const neckWidth = Utils.MathDistance(neckLeft, neckRight); 130 | const widthDif = (tailWidth - neckWidth) / 2; 131 | let [tempLen, leftBodyPnts, rightBodyPnts] = [0, [], []]; 132 | for (let i = 1; i < points.length - 1; i++) { 133 | const angle = Utils.getAngleOfThreePoints(points[i - 1], points[i], points[i + 1]) / 2; 134 | tempLen += Utils.MathDistance(points[i - 1], points[i]); 135 | const w = (tailWidth / 2 - (tempLen / allLen) * widthDif) / Math.sin(angle); 136 | const left = Utils.getThirdPoint(points[i - 1], points[i], Math.PI - angle, w, true); 137 | const right = Utils.getThirdPoint(points[i - 1], points[i], angle, w, false); 138 | leftBodyPnts.push(left); 139 | rightBodyPnts.push(right); 140 | } 141 | return leftBodyPnts.concat(rightBodyPnts); 142 | } 143 | 144 | /** 145 | * In edit mode, drag key points to update corresponding key point data. 146 | */ 147 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 148 | this.points[index] = cartesian; 149 | const geometryPoints = this.createGraphic(this.points); 150 | this.setGeometryPoints(geometryPoints); 151 | this.drawPolygon(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cesium-plot-js 2 | 3 | cesium 军事标绘插件,支持绘制多边形、曲线、箭头等图形 4 | 5 | ![image](https://ethan-zf.github.io/cesium-plot-js/examples/banner.png) 6 | 7 | 淡入淡出效果: 8 | 9 | ![image](https://ethan-zf.github.io/cesium-plot-js/examples/show-hide-animation.gif) 10 | 11 | 生长动画: 12 | ![image](https://ethan-zf.github.io/cesium-plot-js/examples/attack-arrow-growth.gif) 13 | 14 | 在线示例: [demo](https://ethan-zf.github.io/cesium-plot-js/examples/index.html) 15 | 16 | ### CDN 17 | 18 | 1. 引入文件 19 | 20 | ``` 21 | 22 | ``` 23 | 24 | 2. 调用绘制 api 25 | 26 | ``` 27 | new CesiumPlot.FineArrow(Cesium, viewer); 28 | ``` 29 | 30 | ### NPM 31 | 32 | 1. install 33 | 34 | ``` 35 | npm i cesium-plot-js 36 | ``` 37 | 38 | 2. import 39 | 40 | ``` 41 | import CesiumPlot from 'cesium-plot-js'; 42 | ``` 43 | 44 | 3. 调用绘制 api 45 | 46 | ``` 47 | new CesiumPlot.FineArrow(Cesium, viewer); 48 | ``` 49 | 50 | ### Classes 51 | 52 | 每个图形为独立的类,绑定事件或其他操作通过类的实例来实现 53 | 54 | | 类名 | 类型 | 描述 | 生长动画 | 55 | | ---------------------- | --------- | ---------------- | -------- | 56 | | Polygon | 'polygon' | 多边形 | ❌ | 57 | | Reactangle | 'polygon' | 矩形 | ❌ | 58 | | Triangle | 'polygon' | 三角形 | ❌ | 59 | | Circle | 'polygon' | 圆形 | ❌ | 60 | | Sector | 'polygon' | 扇形 | ❌ | 61 | | StraightArrow | 'line' | 细直箭头 | ✔️ | 62 | | CurvedArrow | 'line' | 曲线箭头 | ✔️ | 63 | | FineArrow | 'polygon' | 直箭头 | ✔️ | 64 | | AttackArrow | 'polygon' | 进攻方向箭头 | ✔️ | 65 | | SwallowtailAttackArrow | 'polygon' | 燕尾进攻方向箭头 | ✔️ | 66 | | SquadCombat | 'polygon' | 分队战斗方向 | ✔️ | 67 | | SwallowtailSquadCombat | 'polygon' | 燕尾分队战斗方向 | ✔️ | 68 | | AssaultDirection | 'polygon' | 突击方向 | ✔️ | 69 | | DoubleArrow | 'polygon' | 双箭头 | ✔️ | 70 | | FreehandLine | 'line' | 自由线 | ❌ | 71 | | FreehandPolygon | 'polygon' | 自由面 | ❌ | 72 | | Curve | 'line' | 曲线 | ❌ | 73 | | Ellipse | 'polygon' | 椭圆 | ❌ | 74 | | Lune | 'polygon' | 半月面 | ❌ | 75 | 76 | ### 构造函数 77 | 78 | 所有图形的构造函数: 79 | 80 | <类名>(cesium: Cesium, viewer: Cesium.Viewer, style?: [PolygonStyle](#PolygonStyle) | [LineStyle](#LineStyle)) 81 | 82 |
PolygonStyle类型
83 | 84 | ``` 85 | { 86 | material?: Cesium.MaterialProperty; 87 | outlineWidth?: number; 88 | outlineMaterial?: Cesium.MaterialProperty; 89 | }; 90 | ``` 91 | 92 |
LineStyle类型
93 | 94 | ``` 95 | { 96 | material?: Cesium.Color; 97 | lineWidth?: number; 98 | }; 99 | ``` 100 | 101 | 示例 102 | 103 | ``` 104 | // 初始化viewer 105 | const viewer = new Cesium.Viewer('cesiumContainer'); 106 | // 抗锯齿 107 | viewer.scene.postProcessStages.fxaa.enabled = true; 108 | // 设置自定义样式 109 | const geometry = new CesiumPlot.FineArrow(Cesium, viewer, { 110 | material: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 0.5)'), 111 | outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 112 | outlineWidth: 3, 113 | }); 114 | ``` 115 | 116 | ### 类的实例方法 117 | 118 | | 方法名 | 参数 | 描述 | 119 | | -------------------- | --------------------------------------------------------------------- | ---------------------------------------------------- | 120 | | hide | options?: [AnimationOpts](#AnimationOpts) | 隐藏,options 可配置动画参数,参数缺省时,不显示动画 | 121 | | show | options?: [AnimationOpts](#AnimationOpts) | 显示,options 可配置动画参数,参数缺省时,不显示动画 | 122 | | startGrowthAnimation | options?: [AnimationOpts](#AnimationOpts) | 生长动画,options 可配置动画参数 | 123 | | getPoints | | 获取图形关键点位 | 124 | | remove | | 删除 | 125 | | on | (event: [EventType](#EventType), listener: (eventData?: any) => void) | 绑定事件 | 126 | | off | (event: [EventType](#EventType)) | 解绑事件 | 127 | 128 |
AnimationOpts类型
129 | 130 | | 参数 | 类型 | 默认值 | 描述 | 131 | | -------- | ---------- | ------ | ---------------------- | 132 | | duration | number | 2000 | 动画持续时间(ms) | 133 | | delay | number | 0 | 动画延迟启动时间(ms) | 134 | | callback | () => void | - | 动画结束回调 | 135 | 136 | ``` 137 | // 隐藏图形 138 | const geometry = new CesiumPlot.Reactangle(Cesium, viewer); 139 | geometry.hide(); 140 | ``` 141 | 142 | ``` 143 | // 绑定事件 144 | const geometry = new CesiumPlot.Reactangle(Cesium, viewer); 145 | geometry.on('drawEnd', (data)=>{ 146 | console.log(data) 147 | }); 148 | ``` 149 | 150 | ### 静态方法 151 | 152 | **CesiumPlot.createGeometryFromData(cesium: Cesium, viewer: Cesium.Viewer, options:[CreateGeometryFromDataOpts](#CreateGeometryFromDataOpts))** 153 | 154 | 根据图形的关键点位重新生成图形 155 | 156 |
CreateGeometryFromDataOpts参数类型
157 | 158 | ``` 159 | { 160 | type: 'FineArrow'|'AttackArrow'|'SwallowtailAttackArrow'|'SquadCombat'|'SwallowtailSquadCombat'|'StraightArrow'|'CurvedArrow'|'AssaultDirection'|'DoubleArrow'|'FreehandLine'|'FreehandPolygon'|'Curve'|'Ellipse'|'Lune'|'Reactangle'|'Triangle'|'Polygon'|'Circle'|'Sector', // 图形类型 161 | cartesianPoints: Cesium.Cartesian3[], // 图形关键点位,可通过实例方法getPoints或者drawEnd事件获得 162 | style?: PolygonStyle | LineStyle // 样式 163 | } 164 | ``` 165 | 166 |

Events

167 | 168 | - **drawStart** 169 | 170 | 绘制开始 171 | 172 | - **drawUpdate** 173 | 174 | 绘制过程中点位更新,回调事件返回更新的 Cartesian3 点位 175 | 176 | - **drawEnd** 177 | 178 | 绘制结束,回调事件返回图形的关键点位 179 | 180 | ``` 181 | geometry.on('drawEnd', (data) => { 182 | console.log(data); 183 | }); 184 | ``` 185 | 186 | - **editStart** 187 | 188 | 编辑开始 189 | 190 | - **editEnd** 191 | 192 | 编辑结束,回调事件返回图形的关键点位 193 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | window.onload = () => { 2 | let raster = new Cesium.ArcGisMapServerImageryProvider({ 3 | url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer', 4 | }); 5 | 6 | const viewer = new Cesium.Viewer('cesiumContainer', { 7 | animation: false, 8 | shouldAnimate: true, 9 | geocoder: false, 10 | homeButton: false, 11 | infoBox: false, 12 | fullscreenButton: false, 13 | sceneModePicker: false, 14 | selectionIndicator: false, 15 | timeline: false, 16 | navigationHelpButton: false, 17 | baseLayerPicker: false, 18 | imageryProvider: raster, 19 | contextOptions: { 20 | requestWebgl2: true, 21 | }, 22 | }); 23 | 24 | viewer.scene.postProcessStages.fxaa.enabled = true; 25 | viewer.scene.camera.setView({ 26 | destination: Cesium.Cartesian3.fromDegrees(107.857, 35.594498, 10000), 27 | }); 28 | // let scene = viewer.scene; 29 | // viewer.scene.debugShowFramesPerSecond = true; 30 | 31 | // viewer.camera.flyTo({ 32 | // destination: new Cesium.Cartesian3(-2480561.3182985717, 4681691.324170088, 3539464.2534263907), 33 | // orientation: { 34 | // heading: 2.0002851646951996, 35 | // pitch: -0.25963088874216256, 36 | // roll: 6.283183024299778, 37 | // }, 38 | // complete: function () { 39 | // console.error('fly complete'); 40 | // }, 41 | // }); 42 | 43 | // // let geometry = new CesiumPlot.Polygon(Cesium, viewer); 44 | // let geometry = new CesiumPlot.FineArrow(Cesium, viewer, { 45 | // material: Cesium.Color.fromCssColorString('rgba(255, 178, 208, 0.5)'), 46 | // outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 47 | // outlineWidth: 3, 48 | // }); 49 | let geometry; 50 | const dragStartHandler = () => { 51 | console.error('start'); 52 | }; 53 | const drawUpdateHandler = (cartesian) => { 54 | console.error('update', cartesian); 55 | }; 56 | 57 | const drawEndHandler = (geometryPoints) => { 58 | console.error('drawEnd', geometryPoints); 59 | }; 60 | 61 | const editStartHandler = () => { 62 | console.error('editStart'); 63 | }; 64 | 65 | const editEndHandler = (geometryPoints) => { 66 | console.error('editEnd', geometryPoints); 67 | }; 68 | const buttonGroup = document.getElementById('button-group'); 69 | buttonGroup.onclick = (evt) => { 70 | const targetElement = evt.target; 71 | switch (targetElement.id) { 72 | case 'drawCircle': 73 | geometry = new CesiumPlot.Circle(Cesium, viewer); 74 | break; 75 | case 'drawPolygon': 76 | geometry = new CesiumPlot.Polygon(Cesium, viewer); 77 | break; 78 | case 'drawReactangle': 79 | geometry = new CesiumPlot.Reactangle(Cesium, viewer); 80 | break; 81 | case 'drawTriangle': 82 | geometry = new CesiumPlot.Triangle(Cesium, viewer); 83 | break; 84 | case 'drawFineArrow': 85 | geometry = new CesiumPlot.FineArrow(Cesium, viewer, { 86 | // material: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 0.5)'), 87 | // outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 88 | // outlineWidth: 3, 89 | }); 90 | break; 91 | case 'drawAttackArrow': 92 | geometry = new CesiumPlot.AttackArrow(Cesium, viewer, { 93 | // outlineMaterial: Cesium.Color.RED, 94 | }); 95 | break; 96 | case 'drawSwallowtailAttackArrow': 97 | geometry = new CesiumPlot.SwallowtailAttackArrow(Cesium, viewer, { 98 | // outlineMaterial: Cesium.Color.BLUE, 99 | }); 100 | break; 101 | case 'drawSquadCombat': 102 | geometry = new CesiumPlot.SquadCombat(Cesium, viewer, { 103 | // outlineMaterial: new Cesium.PolylineDashMaterialProperty({ 104 | // color: Cesium.Color.RED, 105 | // dashLength: 16.0, 106 | // }), 107 | }); 108 | break; 109 | case 'drawSwallowtailSquadCombat': 110 | geometry = new CesiumPlot.SwallowtailSquadCombat(Cesium, viewer); 111 | break; 112 | case 'drawStraightArrow': 113 | geometry = new CesiumPlot.StraightArrow(Cesium, viewer, { 114 | // material: Cesium.Color.RED, 115 | }); 116 | break; 117 | case 'drawAssaultDirection': 118 | geometry = new CesiumPlot.AssaultDirection(Cesium, viewer); 119 | break; 120 | case 'drawCurvedArrow': 121 | geometry = new CesiumPlot.CurvedArrow(Cesium, viewer, { 122 | // material: Cesium.Color.BLUE, 123 | }); 124 | break; 125 | case 'drawDoubleArrow': 126 | geometry = new CesiumPlot.DoubleArrow(Cesium, viewer, { 127 | // outlineMaterial: Cesium.Color.GREEN, 128 | }); 129 | break; 130 | case 'drawFreehandLine': 131 | geometry = new CesiumPlot.FreehandLine(Cesium, viewer); 132 | break; 133 | case 'drawCurve': 134 | geometry = new CesiumPlot.Curve(Cesium, viewer); 135 | break; 136 | case 'drawEllipse': 137 | geometry = new CesiumPlot.Ellipse(Cesium, viewer); 138 | break; 139 | case 'drawLune': 140 | geometry = new CesiumPlot.Lune(Cesium, viewer); 141 | break; 142 | case 'drawFreehandPolygon': 143 | geometry = new CesiumPlot.FreehandPolygon(Cesium, viewer, { 144 | // material: Cesium.Color.GREEN, 145 | // outlineMaterial: Cesium.Color.fromCssColorString('rgba(59, 178, 208, 1)'), 146 | // outlineWidth: 2, 147 | }); 148 | break; 149 | case 'hide': 150 | geometry && 151 | geometry.hide({ 152 | deration: 2000, 153 | }); 154 | break; 155 | case 'show': 156 | geometry && 157 | geometry.show({ 158 | deration: 2000, 159 | }); 160 | break; 161 | case 'remove': 162 | geometry && geometry.remove(); 163 | geometry = null; 164 | break; 165 | case 'addEvent': 166 | if (geometry) { 167 | geometry.on('drawStart', dragStartHandler); 168 | geometry.on('drawUpdate', drawUpdateHandler); 169 | geometry.on('drawEnd', drawEndHandler); 170 | geometry.on('editStart', editStartHandler); 171 | geometry.on('editEnd', editEndHandler); 172 | } 173 | break; 174 | case 'removeEvent': 175 | if (geometry) { 176 | geometry.off('drawStart', dragStartHandler); 177 | geometry.off('drawUpdate', drawUpdateHandler); 178 | geometry.off('drawEnd', drawEndHandler); 179 | geometry.off('editStart', editStartHandler); 180 | geometry.off('editEnd', editEndHandler); 181 | } 182 | break; 183 | case 'startGrowthAnimation': 184 | if (geometry) { 185 | geometry.startGrowthAnimation({ 186 | duration: 2000, 187 | }); 188 | } 189 | break; 190 | default: 191 | break; 192 | } 193 | }; 194 | }; 195 | -------------------------------------------------------------------------------- /src/arrow/double-arrow.ts: -------------------------------------------------------------------------------- 1 | import Base from '../base'; 2 | import * as Utils from '../utils'; 3 | // @ts-ignore 4 | import { Cartesian3 } from 'cesium'; 5 | import { PolygonStyle } from '../interface'; 6 | type Position = [number, number]; 7 | 8 | export default class DoubleArrow extends Base { 9 | points: Cartesian3[] = []; 10 | arrowLengthScale: number = 5; 11 | maxArrowLength: number = 2; 12 | neckWidthFactor: number; 13 | headWidthFactor: number; 14 | headHeightFactor: number; 15 | neckHeightFactor: number; 16 | connPoint: Position; 17 | tempPoint4: Position; 18 | minPointsForShape: number; 19 | llBodyPnts: Position[] = []; 20 | rrBodyPnts: Position[] = []; 21 | curveControlPointLeft: Cartesian3; 22 | curveControlPointRight: Cartesian3; 23 | isClockWise: boolean; 24 | 25 | constructor(cesium: any, viewer: any, style?: PolygonStyle) { 26 | super(cesium, viewer, style); 27 | this.cesium = cesium; 28 | this.headHeightFactor = 0.25; 29 | this.headWidthFactor = 0.3; 30 | this.neckHeightFactor = 0.85; 31 | this.neckWidthFactor = 0.15; 32 | this.connPoint = [0, 0]; 33 | this.tempPoint4 = [0, 0]; 34 | this.minPointsForShape = 4; 35 | this.setState('drawing'); 36 | } 37 | 38 | getType(): 'polygon' | 'line' { 39 | return 'polygon'; 40 | } 41 | 42 | /** 43 | * Add points only on click events 44 | */ 45 | addPoint(cartesian: Cartesian3) { 46 | this.points.push(cartesian); 47 | if (this.points.length < 2) { 48 | this.onMouseMove(); 49 | } else if (this.points.length === 2) { 50 | this.setGeometryPoints(this.points); 51 | this.drawPolygon(); 52 | } else if (this.points.length === 3) { 53 | this.lineEntity && this.viewer.entities.remove(this.lineEntity); 54 | } else { 55 | this.finishDrawing(); 56 | 57 | // // 辅助查看插值控制点位置 58 | // this.viewer.entities.add({ 59 | // position: this.curveControlPointLeft, 60 | // point: { 61 | // pixelSize: 10, 62 | // heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND, 63 | // color: this.cesium.Color.RED, 64 | // }, 65 | // }); 66 | // this.viewer.entities.add({ 67 | // position: this.curveControlPointRight, 68 | // point: { 69 | // pixelSize: 10, 70 | // heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND, 71 | // color: this.cesium.Color.RED, 72 | // }, 73 | // }); 74 | } 75 | } 76 | 77 | finishDrawing() { 78 | this.curveControlPointLeft = this.cesium.Cartesian3.fromDegrees(this.llBodyPnts[2][0], this.llBodyPnts[2][1]); 79 | this.curveControlPointRight = this.cesium.Cartesian3.fromDegrees(this.rrBodyPnts[1][0], this.rrBodyPnts[1][1]); 80 | super.finishDrawing(); 81 | } 82 | /** 83 | * Draw a shape based on mouse movement points during the initial drawing. 84 | */ 85 | updateMovingPoint(cartesian: Cartesian3) { 86 | const tempPoints = [...this.points, cartesian]; 87 | this.setGeometryPoints(tempPoints); 88 | if (tempPoints.length === 2) { 89 | this.addTempLine(); 90 | } else if (tempPoints.length > 2) { 91 | this.removeTempLine(); 92 | const geometryPoints = this.createGraphic(tempPoints); 93 | this.setGeometryPoints(geometryPoints); 94 | this.drawPolygon(); 95 | } 96 | } 97 | 98 | /** 99 | * In edit mode, drag key points to update corresponding key point data. 100 | */ 101 | updateDraggingPoint(cartesian: Cartesian3, index: number) { 102 | this.points[index] = cartesian; 103 | const geometryPoints = this.createGraphic(this.points); 104 | this.setGeometryPoints(geometryPoints); 105 | this.drawPolygon(); 106 | } 107 | 108 | /** 109 | * Generate geometric shapes based on key points. 110 | */ 111 | createGraphic(positions: Cartesian3[]) { 112 | const lnglatPoints = positions.map((pnt) => { 113 | return this.cartesianToLnglat(pnt); 114 | }); 115 | const [pnt1, pnt2, pnt3] = [lnglatPoints[0], lnglatPoints[1], lnglatPoints[2]]; 116 | const count = lnglatPoints.length; 117 | if (count === 3) { 118 | this.tempPoint4 = this.getTempPoint4(pnt1, pnt2, pnt3); 119 | this.connPoint = Utils.Mid(pnt1, pnt2); 120 | } else if (count === 4) { 121 | this.tempPoint4 = lnglatPoints[3]; 122 | this.connPoint = Utils.Mid(pnt1, pnt2); 123 | } else { 124 | this.tempPoint4 = lnglatPoints[3]; 125 | this.connPoint = lnglatPoints[4]; 126 | } 127 | let leftArrowPnts: Position[]; 128 | let rightArrowPnts; 129 | this.isClockWise = Utils.isClockWise(pnt1, pnt2, pnt3); 130 | if (this.isClockWise) { 131 | leftArrowPnts = this.getArrowPoints(pnt1, this.connPoint, this.tempPoint4, false); 132 | rightArrowPnts = this.getArrowPoints(this.connPoint, pnt2, pnt3, true); 133 | } else { 134 | leftArrowPnts = this.getArrowPoints(pnt2, this.connPoint, pnt3, false); 135 | rightArrowPnts = this.getArrowPoints(this.connPoint, pnt1, this.tempPoint4, true); 136 | } 137 | const m = leftArrowPnts.length; 138 | const t = (m - 5) / 2; 139 | const llBodyPnts = leftArrowPnts.slice(0, t); 140 | const lArrowPnts = leftArrowPnts.slice(t, t + 5); 141 | let lrBodyPnts = leftArrowPnts.slice(t + 5, m); 142 | this.llBodyPnts = llBodyPnts; 143 | let rlBodyPnts = rightArrowPnts.slice(0, t); 144 | const rArrowPnts = rightArrowPnts.slice(t, t + 5); 145 | const rrBodyPnts = rightArrowPnts.slice(t + 5, m); 146 | this.rrBodyPnts = rrBodyPnts; 147 | rlBodyPnts = Utils.getBezierPoints(rlBodyPnts); 148 | const bodyPnts = Utils.getBezierPoints(rrBodyPnts.concat(llBodyPnts.slice(1))); 149 | lrBodyPnts = Utils.getBezierPoints(lrBodyPnts); 150 | const pnts = rlBodyPnts.concat(rArrowPnts, bodyPnts, lArrowPnts, lrBodyPnts); 151 | const temp = [].concat(...pnts); 152 | const cartesianPoints = this.cesium.Cartesian3.fromDegreesArray(temp); 153 | return cartesianPoints; 154 | } 155 | 156 | getTempPoint4(linePnt1: Position, linePnt2: Position, point: Position): Position { 157 | const midPnt = Utils.Mid(linePnt1, linePnt2); 158 | const len = Utils.MathDistance(midPnt, point); 159 | const angle = Utils.getAngleOfThreePoints(linePnt1, midPnt, point); 160 | let symPnt = [0, 0] as Position; 161 | let distance1; 162 | let distance2; 163 | let mid; 164 | if (angle < Math.PI / 2) { 165 | distance1 = len * Math.sin(angle); 166 | distance2 = len * Math.cos(angle); 167 | mid = Utils.getThirdPoint(linePnt1, midPnt, Math.PI / 2, distance1, false); 168 | symPnt = Utils.getThirdPoint(midPnt, mid, Math.PI / 2, distance2, true); 169 | } else if (angle >= Math.PI / 2 && angle < Math.PI) { 170 | distance1 = len * Math.sin(Math.PI - angle); 171 | distance2 = len * Math.cos(Math.PI - angle); 172 | mid = Utils.getThirdPoint(linePnt1, midPnt, Math.PI / 2, distance1, false); 173 | symPnt = Utils.getThirdPoint(midPnt, mid, Math.PI / 2, distance2, false); 174 | } else if (angle >= Math.PI && angle < Math.PI * 1.5) { 175 | distance1 = len * Math.sin(angle - Math.PI); 176 | distance2 = len * Math.cos(angle - Math.PI); 177 | mid = Utils.getThirdPoint(linePnt1, midPnt, Math.PI / 2, distance1, true); 178 | symPnt = Utils.getThirdPoint(midPnt, mid, Math.PI / 2, distance2, true); 179 | } else { 180 | distance1 = len * Math.sin(Math.PI * 2 - angle); 181 | distance2 = len * Math.cos(Math.PI * 2 - angle); 182 | mid = Utils.getThirdPoint(linePnt1, midPnt, Math.PI / 2, distance1, true); 183 | symPnt = Utils.getThirdPoint(midPnt, mid, Math.PI / 2, distance2, false); 184 | } 185 | return symPnt; 186 | } 187 | 188 | getArrowPoints(pnt1: Position, pnt2: Position, pnt3: Position, clockWise: boolean): Position[] { 189 | const midPnt = Utils.Mid(pnt1, pnt2); 190 | const len = Utils.MathDistance(midPnt, pnt3); 191 | let midPnt1 = Utils.getThirdPoint(pnt3, midPnt, 0, len * 0.3, true); 192 | let midPnt2 = Utils.getThirdPoint(pnt3, midPnt, 0, len * 0.5, true); 193 | midPnt1 = Utils.getThirdPoint(midPnt, midPnt1, Math.PI / 2, len / 5, clockWise); 194 | 195 | midPnt2 = Utils.getThirdPoint(midPnt, midPnt2, Math.PI / 2, len / 4, clockWise); 196 | const points = [midPnt, midPnt1, midPnt2, pnt3]; 197 | const arrowPnts = this.getArrowHeadPoints(points); 198 | if (arrowPnts && Array.isArray(arrowPnts) && arrowPnts.length > 0) { 199 | const neckLeftPoint: Position = arrowPnts[0]; 200 | const neckRightPoint: Position = arrowPnts[4]; 201 | const tailWidthFactor = Utils.MathDistance(pnt1, pnt2) / Utils.getBaseLength(points) / 2; 202 | const bodyPnts = this.getArrowBodyPoints(points, neckLeftPoint, neckRightPoint, tailWidthFactor); 203 | if (bodyPnts) { 204 | const n = bodyPnts.length; 205 | let lPoints = bodyPnts.slice(0, n / 2); 206 | let rPoints = bodyPnts.slice(n / 2, n); 207 | lPoints.push(neckLeftPoint); 208 | rPoints.push(neckRightPoint); 209 | lPoints = lPoints.reverse(); 210 | lPoints.push(pnt2); 211 | rPoints = rPoints.reverse(); 212 | rPoints.push(pnt1); 213 | return lPoints.reverse().concat(arrowPnts, rPoints); 214 | } 215 | } else { 216 | throw new Error('Interpolation Error'); 217 | } 218 | } 219 | 220 | getArrowBodyPoints(points: Position[], neckLeft: Position, neckRight: Position, tailWidthFactor: number): Position[] { 221 | const allLen = Utils.wholeDistance(points); 222 | const len = Utils.getBaseLength(points); 223 | const tailWidth = len * tailWidthFactor; 224 | const neckWidth = Utils.MathDistance(neckLeft, neckRight); 225 | const widthDif = (tailWidth - neckWidth) / 2; 226 | let tempLen: number = 0; 227 | let leftBodyPnts: Position[] = []; 228 | let rightBodyPnts: Position[] = []; 229 | for (let i = 1; i < points.length - 1; i++) { 230 | const angle = Utils.getAngleOfThreePoints(points[i - 1], points[i], points[i + 1]) / 2; 231 | tempLen += Utils.MathDistance(points[i - 1], points[i]); 232 | const w = (tailWidth / 2 - (tempLen / allLen) * widthDif) / Math.sin(angle); 233 | const left = Utils.getThirdPoint(points[i - 1], points[i], Math.PI - angle, w, true); 234 | const right = Utils.getThirdPoint(points[i - 1], points[i], angle, w, false); 235 | leftBodyPnts.push(left); 236 | rightBodyPnts.push(right); 237 | } 238 | return leftBodyPnts.concat(rightBodyPnts); 239 | } 240 | 241 | getArrowHeadPoints(points: Position[]): Position[] { 242 | const len = Utils.getBaseLength(points); 243 | const headHeight = len * this.headHeightFactor; 244 | const headPnt = points[points.length - 1]; 245 | const headWidth = headHeight * this.headWidthFactor; 246 | const neckWidth = headHeight * this.neckWidthFactor; 247 | const neckHeight = headHeight * this.neckHeightFactor; 248 | const headEndPnt = Utils.getThirdPoint(points[points.length - 2], headPnt, 0, headHeight, true); 249 | const neckEndPnt = Utils.getThirdPoint(points[points.length - 2], headPnt, 0, neckHeight, true); 250 | const headLeft = Utils.getThirdPoint(headPnt, headEndPnt, Math.PI / 2, headWidth, false); 251 | const headRight = Utils.getThirdPoint(headPnt, headEndPnt, Math.PI / 2, headWidth, true); 252 | const neckLeft = Utils.getThirdPoint(headPnt, neckEndPnt, Math.PI / 2, neckWidth, false); 253 | const neckRight = Utils.getThirdPoint(headPnt, neckEndPnt, Math.PI / 2, neckWidth, true); 254 | return [neckLeft, headLeft, headPnt, headRight, neckRight]; 255 | } 256 | 257 | getPoints() { 258 | return this.points; 259 | } 260 | 261 | getBezierControlPointforGrowthAnimation() { 262 | return this.isClockWise 263 | ? { 264 | left: this.curveControlPointLeft, 265 | right: this.curveControlPointRight, 266 | } 267 | : { 268 | right: this.curveControlPointLeft, 269 | left: this.curveControlPointRight, 270 | }; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const FITTING_COUNT = 100; 2 | const ZERO_TOLERANCE = 0.0001; 3 | 4 | /** 5 | * 计算两个坐标之间的距离 6 | * @param pnt1 7 | * @param pnt2 8 | * @returns {number} 9 | * @constructor 10 | */ 11 | export const MathDistance = (pnt1, pnt2) => Math.sqrt((pnt1[0] - pnt2[0]) ** 2 + (pnt1[1] - pnt2[1]) ** 2); 12 | 13 | /** 14 | * 计算点集合的总距离 15 | * @param points 16 | * @returns {number} 17 | */ 18 | export const wholeDistance = (points) => { 19 | let distance = 0; 20 | if (points && Array.isArray(points) && points.length > 0) { 21 | points.forEach((item, index) => { 22 | if (index < points.length - 1) { 23 | distance += MathDistance(item, points[index + 1]); 24 | } 25 | }); 26 | } 27 | return distance; 28 | }; 29 | /** 30 | * 获取基础长度 31 | * @param points 32 | * @returns {number} 33 | */ 34 | export const getBaseLength = (points) => wholeDistance(points) ** 0.99; 35 | 36 | /** 37 | * 求取两个坐标的中间值 38 | * @param point1 39 | * @param point2 40 | * @returns {[*,*]} 41 | * @constructor 42 | */ 43 | export const Mid = (point1, point2): [number, number] => [(point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2]; 44 | 45 | /** 46 | * 通过三个点确定一个圆的中心点 47 | * @param point1 48 | * @param point2 49 | * @param point3 50 | */ 51 | export const getCircleCenterOfThreePoints = (point1, point2, point3) => { 52 | const pntA = [(point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2]; 53 | const pntB = [pntA[0] - point1[1] + point2[1], pntA[1] + point1[0] - point2[0]]; 54 | const pntC = [(point1[0] + point3[0]) / 2, (point1[1] + point3[1]) / 2]; 55 | const pntD = [pntC[0] - point1[1] + point3[1], pntC[1] + point1[0] - point3[0]]; 56 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 57 | return getIntersectPoint(pntA, pntB, pntC, pntD); 58 | }; 59 | 60 | /** 61 | * 获取交集的点 62 | * @param pntA 63 | * @param pntB 64 | * @param pntC 65 | * @param pntD 66 | * @returns {[*,*]} 67 | */ 68 | export const getIntersectPoint = (pntA, pntB, pntC, pntD) => { 69 | if (pntA[1] === pntB[1]) { 70 | const f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1]); 71 | const x = f * (pntA[1] - pntC[1]) + pntC[0]; 72 | const y = pntA[1]; 73 | return [x, y]; 74 | } 75 | if (pntC[1] === pntD[1]) { 76 | const e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1]); 77 | const x = e * (pntC[1] - pntA[1]) + pntA[0]; 78 | const y = pntC[1]; 79 | return [x, y]; 80 | } 81 | const e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1]); 82 | const f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1]); 83 | const y = (e * pntA[1] - pntA[0] - f * pntC[1] + pntC[0]) / (e - f); 84 | const x = e * y - e * pntA[1] + pntA[0]; 85 | return [x, y]; 86 | }; 87 | 88 | /** 89 | * 获取方位角(地平经度) 90 | * @param startPoint 91 | * @param endPoint 92 | * @returns {*} 93 | */ 94 | export const getAzimuth = (startPoint, endPoint) => { 95 | let azimuth; 96 | const angle = Math.asin(Math.abs(endPoint[1] - startPoint[1]) / MathDistance(startPoint, endPoint)); 97 | if (endPoint[1] >= startPoint[1] && endPoint[0] >= startPoint[0]) { 98 | azimuth = angle + Math.PI; 99 | } else if (endPoint[1] >= startPoint[1] && endPoint[0] < startPoint[0]) { 100 | azimuth = Math.PI * 2 - angle; 101 | } else if (endPoint[1] < startPoint[1] && endPoint[0] < startPoint[0]) { 102 | azimuth = angle; 103 | } else if (endPoint[1] < startPoint[1] && endPoint[0] >= startPoint[0]) { 104 | azimuth = Math.PI - angle; 105 | } 106 | return azimuth; 107 | }; 108 | 109 | /** 110 | * 通过三个点获取方位角 111 | * @param pntA 112 | * @param pntB 113 | * @param pntC 114 | * @returns {number} 115 | */ 116 | export const getAngleOfThreePoints = (pntA, pntB, pntC) => { 117 | const angle = getAzimuth(pntB, pntA) - getAzimuth(pntB, pntC); 118 | return angle < 0 ? angle + Math.PI * 2 : angle; 119 | }; 120 | 121 | /** 122 | * 判断是否是顺时针 123 | * @param pnt1 124 | * @param pnt2 125 | * @param pnt3 126 | * @returns {boolean} 127 | */ 128 | export const isClockWise = (pnt1, pnt2, pnt3) => 129 | (pnt3[1] - pnt1[1]) * (pnt2[0] - pnt1[0]) > (pnt2[1] - pnt1[1]) * (pnt3[0] - pnt1[0]); 130 | 131 | /** 132 | * 获取线上的点 133 | * @param t 134 | * @param startPnt 135 | * @param endPnt 136 | * @returns {[*,*]} 137 | */ 138 | export const getPointOnLine = (t, startPnt, endPnt) => { 139 | const x = startPnt[0] + t * (endPnt[0] - startPnt[0]); 140 | const y = startPnt[1] + t * (endPnt[1] - startPnt[1]); 141 | return [x, y]; 142 | }; 143 | 144 | /** 145 | * 获取立方值 146 | * @param t 147 | * @param startPnt 148 | * @param cPnt1 149 | * @param cPnt2 150 | * @param endPnt 151 | * @returns {[*,*]} 152 | */ 153 | export const getCubicValue = (t, startPnt, cPnt1, cPnt2, endPnt) => { 154 | // eslint-disable-next-line no-param-reassign 155 | t = Math.max(Math.min(t, 1), 0); 156 | const [tp, t2] = [1 - t, t * t]; 157 | const t3 = t2 * t; 158 | const tp2 = tp * tp; 159 | const tp3 = tp2 * tp; 160 | const x = tp3 * startPnt[0] + 3 * tp2 * t * cPnt1[0] + 3 * tp * t2 * cPnt2[0] + t3 * endPnt[0]; 161 | const y = tp3 * startPnt[1] + 3 * tp2 * t * cPnt1[1] + 3 * tp * t2 * cPnt2[1] + t3 * endPnt[1]; 162 | return [x, y]; 163 | }; 164 | 165 | /** 166 | * 根据起止点和旋转方向求取第三个点 167 | * @param startPnt 168 | * @param endPnt 169 | * @param angle 170 | * @param distance 171 | * @param clockWise 172 | * @returns {[*,*]} 173 | */ 174 | export const getThirdPoint = (startPnt, endPnt, angle, distance, clockWise): [number, number] => { 175 | const azimuth = getAzimuth(startPnt, endPnt); 176 | const alpha = clockWise ? azimuth + angle : azimuth - angle; 177 | const dx = distance * Math.cos(alpha); 178 | const dy = distance * Math.sin(alpha); 179 | return [endPnt[0] + dx, endPnt[1] + dy]; 180 | }; 181 | 182 | /** 183 | * 插值弓形线段点 184 | * @param center 185 | * @param radius 186 | * @param startAngle 187 | * @param endAngle 188 | * @returns {null} 189 | */ 190 | export const getArcPoints = (center, radius, startAngle, endAngle) => { 191 | // eslint-disable-next-line 192 | let [x, y, pnts, angleDiff] = [null, null, [], endAngle - startAngle]; 193 | angleDiff = angleDiff < 0 ? angleDiff + Math.PI * 2 : angleDiff; 194 | for (let i = 0; i <= 100; i++) { 195 | const angle = startAngle + (angleDiff * i) / 100; 196 | x = center[0] + radius * Math.cos(angle); 197 | y = center[1] + radius * Math.sin(angle); 198 | pnts.push([x, y]); 199 | } 200 | return pnts; 201 | }; 202 | 203 | /** 204 | * getBisectorNormals 205 | * @param t 206 | * @param pnt1 207 | * @param pnt2 208 | * @param pnt3 209 | * @returns {[*,*]} 210 | */ 211 | export const getBisectorNormals = (t, pnt1, pnt2, pnt3) => { 212 | // eslint-disable-next-line 213 | const normal = getNormal(pnt1, pnt2, pnt3); 214 | let [bisectorNormalRight, bisectorNormalLeft, dt, x, y] = [null, null, null, null, null]; 215 | const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]); 216 | const uX = normal[0] / dist; 217 | const uY = normal[1] / dist; 218 | const d1 = MathDistance(pnt1, pnt2); 219 | const d2 = MathDistance(pnt2, pnt3); 220 | if (dist > ZERO_TOLERANCE) { 221 | if (isClockWise(pnt1, pnt2, pnt3)) { 222 | dt = t * d1; 223 | x = pnt2[0] - dt * uY; 224 | y = pnt2[1] + dt * uX; 225 | bisectorNormalRight = [x, y]; 226 | dt = t * d2; 227 | x = pnt2[0] + dt * uY; 228 | y = pnt2[1] - dt * uX; 229 | bisectorNormalLeft = [x, y]; 230 | } else { 231 | dt = t * d1; 232 | x = pnt2[0] + dt * uY; 233 | y = pnt2[1] - dt * uX; 234 | bisectorNormalRight = [x, y]; 235 | dt = t * d2; 236 | x = pnt2[0] - dt * uY; 237 | y = pnt2[1] + dt * uX; 238 | bisectorNormalLeft = [x, y]; 239 | } 240 | } else { 241 | x = pnt2[0] + t * (pnt1[0] - pnt2[0]); 242 | y = pnt2[1] + t * (pnt1[1] - pnt2[1]); 243 | bisectorNormalRight = [x, y]; 244 | x = pnt2[0] + t * (pnt3[0] - pnt2[0]); 245 | y = pnt2[1] + t * (pnt3[1] - pnt2[1]); 246 | bisectorNormalLeft = [x, y]; 247 | } 248 | return [bisectorNormalRight, bisectorNormalLeft]; 249 | }; 250 | 251 | /** 252 | * 获取默认三点的内切圆 253 | * @param pnt1 254 | * @param pnt2 255 | * @param pnt3 256 | * @returns {[*,*]} 257 | */ 258 | export const getNormal = (pnt1, pnt2, pnt3) => { 259 | let dX1 = pnt1[0] - pnt2[0]; 260 | let dY1 = pnt1[1] - pnt2[1]; 261 | const d1 = Math.sqrt(dX1 * dX1 + dY1 * dY1); 262 | dX1 /= d1; 263 | dY1 /= d1; 264 | let dX2 = pnt3[0] - pnt2[0]; 265 | let dY2 = pnt3[1] - pnt2[1]; 266 | const d2 = Math.sqrt(dX2 * dX2 + dY2 * dY2); 267 | dX2 /= d2; 268 | dY2 /= d2; 269 | const uX = dX1 + dX2; 270 | const uY = dY1 + dY2; 271 | return [uX, uY]; 272 | }; 273 | 274 | /** 275 | * 获取左边控制点 276 | * @param controlPoints 277 | * @param t 278 | * @returns {[*,*]} 279 | */ 280 | export const getLeftMostControlPoint = (controlPoints, t) => { 281 | // eslint-disable-next-line 282 | let [pnt1, pnt2, pnt3, controlX, controlY] = [controlPoints[0], controlPoints[1], controlPoints[2], null, null]; 283 | const pnts = getBisectorNormals(0, pnt1, pnt2, pnt3); 284 | const normalRight = pnts[0]; 285 | const normal = getNormal(pnt1, pnt2, pnt3); 286 | const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]); 287 | if (dist > ZERO_TOLERANCE) { 288 | const mid = Mid(pnt1, pnt2); 289 | const pX = pnt1[0] - mid[0]; 290 | const pY = pnt1[1] - mid[1]; 291 | const d1 = MathDistance(pnt1, pnt2); 292 | const n = 2.0 / d1; 293 | const nX = -n * pY; 294 | const nY = n * pX; 295 | const a11 = nX * nX - nY * nY; 296 | const a12 = 2 * nX * nY; 297 | const a22 = nY * nY - nX * nX; 298 | const dX = normalRight[0] - mid[0]; 299 | const dY = normalRight[1] - mid[1]; 300 | controlX = mid[0] + a11 * dX + a12 * dY; 301 | controlY = mid[1] + a12 * dX + a22 * dY; 302 | } else { 303 | controlX = pnt1[0] + t * (pnt2[0] - pnt1[0]); 304 | controlY = pnt1[1] + t * (pnt2[1] - pnt1[1]); 305 | } 306 | return [controlX, controlY]; 307 | }; 308 | 309 | /** 310 | * 获取右边控制点 311 | * @param controlPoints 312 | * @param t 313 | * @returns {[*,*]} 314 | */ 315 | export const getRightMostControlPoint = (controlPoints, t) => { 316 | const count = controlPoints.length; 317 | const pnt1 = controlPoints[count - 3]; 318 | const pnt2 = controlPoints[count - 2]; 319 | const pnt3 = controlPoints[count - 1]; 320 | const pnts = getBisectorNormals(0, pnt1, pnt2, pnt3); 321 | const normalLeft = pnts[1]; 322 | const normal = getNormal(pnt1, pnt2, pnt3); 323 | const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]); 324 | let [controlX, controlY] = [null, null]; 325 | if (dist > ZERO_TOLERANCE) { 326 | const mid = Mid(pnt2, pnt3); 327 | const pX = pnt3[0] - mid[0]; 328 | const pY = pnt3[1] - mid[1]; 329 | const d1 = MathDistance(pnt2, pnt3); 330 | const n = 2.0 / d1; 331 | const nX = -n * pY; 332 | const nY = n * pX; 333 | const a11 = nX * nX - nY * nY; 334 | const a12 = 2 * nX * nY; 335 | const a22 = nY * nY - nX * nX; 336 | const dX = normalLeft[0] - mid[0]; 337 | const dY = normalLeft[1] - mid[1]; 338 | controlX = mid[0] + a11 * dX + a12 * dY; 339 | controlY = mid[1] + a12 * dX + a22 * dY; 340 | } else { 341 | controlX = pnt3[0] + t * (pnt2[0] - pnt3[0]); 342 | controlY = pnt3[1] + t * (pnt2[1] - pnt3[1]); 343 | } 344 | return [controlX, controlY]; 345 | }; 346 | 347 | /** 348 | * 插值曲线点 349 | * @param t 350 | * @param controlPoints 351 | * @returns {null} 352 | */ 353 | export const getCurvePoints = (t, controlPoints) => { 354 | const leftControl = getLeftMostControlPoint(controlPoints, t); 355 | // eslint-disable-next-line 356 | let [pnt1, pnt2, pnt3, normals, points] = [null, null, null, [leftControl], []]; 357 | for (let i = 0; i < controlPoints.length - 2; i++) { 358 | [pnt1, pnt2, pnt3] = [controlPoints[i], controlPoints[i + 1], controlPoints[i + 2]]; 359 | const normalPoints = getBisectorNormals(t, pnt1, pnt2, pnt3); 360 | normals = normals.concat(normalPoints); 361 | } 362 | const rightControl = getRightMostControlPoint(controlPoints, t); 363 | if (rightControl) { 364 | normals.push(rightControl); 365 | } 366 | for (let i = 0; i < controlPoints.length - 1; i++) { 367 | pnt1 = controlPoints[i]; 368 | pnt2 = controlPoints[i + 1]; 369 | points.push(pnt1); 370 | for (let j = 0; j < FITTING_COUNT; j++) { 371 | const pnt = getCubicValue(j / FITTING_COUNT, pnt1, normals[i * 2], normals[i * 2 + 1], pnt2); 372 | points.push(pnt); 373 | } 374 | points.push(pnt2); 375 | } 376 | return points; 377 | }; 378 | 379 | /** 380 | * 贝塞尔曲线 381 | * @param points 382 | * @returns {*} 383 | */ 384 | export const getBezierPoints = function (points) { 385 | if (points.length <= 2) { 386 | return points; 387 | } 388 | const bezierPoints = []; 389 | const n = points.length - 1; 390 | for (let t = 0; t <= 1; t += 0.01) { 391 | let [x, y] = [0, 0]; 392 | for (let index = 0; index <= n; index++) { 393 | // eslint-disable-next-line 394 | const factor = getBinomialFactor(n, index); 395 | const a = t ** index; 396 | const b = (1 - t) ** (n - index); 397 | x += factor * a * b * points[index][0]; 398 | y += factor * a * b * points[index][1]; 399 | } 400 | bezierPoints.push([x, y]); 401 | } 402 | bezierPoints.push(points[n]); 403 | return bezierPoints; 404 | }; 405 | 406 | /** 407 | * 获取阶乘数据 408 | * @param n 409 | * @returns {number} 410 | */ 411 | export const getFactorial = (n) => { 412 | let result = 1; 413 | switch (n) { 414 | case n <= 1: 415 | result = 1; 416 | break; 417 | case n === 2: 418 | result = 2; 419 | break; 420 | case n === 3: 421 | result = 6; 422 | break; 423 | case n === 24: 424 | result = 24; 425 | break; 426 | case n === 5: 427 | result = 120; 428 | break; 429 | default: 430 | for (let i = 1; i <= n; i++) { 431 | result *= i; 432 | } 433 | break; 434 | } 435 | return result; 436 | }; 437 | 438 | /** 439 | * 获取二项分布 440 | * @param n 441 | * @param index 442 | * @returns {number} 443 | */ 444 | export const getBinomialFactor = (n, index) => getFactorial(n) / (getFactorial(index) * getFactorial(n - index)); 445 | 446 | /** 447 | * 插值线性点 448 | * @param points 449 | * @returns {*} 450 | */ 451 | export const getQBSplinePoints = (points) => { 452 | if (points.length <= 2) { 453 | return points; 454 | } 455 | const [n, bSplinePoints] = [2, []]; 456 | const m = points.length - n - 1; 457 | bSplinePoints.push(points[0]); 458 | for (let i = 0; i <= m; i++) { 459 | for (let t = 0; t <= 1; t += 0.05) { 460 | let [x, y] = [0, 0]; 461 | for (let k = 0; k <= n; k++) { 462 | // eslint-disable-next-line 463 | const factor = getQuadricBSplineFactor(k, t); 464 | x += factor * points[i + k][0]; 465 | y += factor * points[i + k][1]; 466 | } 467 | bSplinePoints.push([x, y]); 468 | } 469 | } 470 | bSplinePoints.push(points[points.length - 1]); 471 | return bSplinePoints; 472 | }; 473 | 474 | /** 475 | * 得到二次线性因子 476 | * @param k 477 | * @param t 478 | * @returns {number} 479 | */ 480 | export const getQuadricBSplineFactor = (k, t) => { 481 | let res = 0; 482 | if (k === 0) { 483 | res = (t - 1) ** 2 / 2; 484 | } else if (k === 1) { 485 | res = (-2 * t ** 2 + 2 * t + 1) / 2; 486 | } else if (k === 2) { 487 | res = t ** 2 / 2; 488 | } 489 | return res; 490 | }; 491 | 492 | /** 493 | * 获取id 494 | * @returns {*|string|!Array.} 495 | */ 496 | export const getuuid = () => { 497 | const [s, hexDigits] = [[], '0123456789abcdef']; 498 | for (let i = 0; i < 36; i++) { 499 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 500 | } 501 | s[14] = '4'; 502 | // eslint-disable-next-line no-bitwise 503 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); 504 | // eslint-disable-next-line no-multi-assign 505 | s[8] = s[13] = s[18] = s[23] = '-'; 506 | return s.join(''); 507 | }; 508 | 509 | /** 510 | * 添加标识 511 | * @param obj 512 | * @returns {*} 513 | */ 514 | export const stamp = function (obj) { 515 | const key = '_event_id_'; 516 | obj[key] = obj[key] || getuuid(); 517 | return obj[key]; 518 | }; 519 | 520 | /** 521 | * 去除字符串前后空格 522 | * @param str 523 | * @returns {*} 524 | */ 525 | export const trim = (str) => (str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '')); 526 | 527 | /** 528 | * 将类名截取成数组 529 | * @param str 530 | * @returns {Array|*} 531 | */ 532 | export const splitWords = (str) => trim(str).split(/\s+/); 533 | 534 | /** 535 | * 判断是否为对象 536 | * @param value 537 | * @returns {boolean} 538 | */ 539 | export const isObject = (value) => { 540 | const type = typeof value; 541 | return value !== null && (type === 'object' || type === 'function'); 542 | }; 543 | 544 | /** 545 | * merge 546 | * @param a 547 | * @param b 548 | * @returns {*} 549 | */ 550 | export const merge = (a, b) => { 551 | // eslint-disable-next-line no-restricted-syntax 552 | for (const key in b) { 553 | if (isObject(b[key]) && isObject(a[key])) { 554 | merge(a[key], b[key]); 555 | } else { 556 | a[key] = b[key]; 557 | } 558 | } 559 | return a; 560 | }; 561 | 562 | export function preventDefault(e) { 563 | // eslint-disable-next-line no-param-reassign 564 | e = e || window.event; 565 | if (e.preventDefault) { 566 | e.preventDefault(); 567 | } else { 568 | e.returnValue = false; 569 | } 570 | } 571 | 572 | export function bindAll(fns, context) { 573 | fns.forEach((fn) => { 574 | if (!context[fn]) { 575 | return; 576 | } 577 | context[fn] = context[fn].bind(context); 578 | }); 579 | } 580 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as CesiumTypeOnly from 'cesium'; 3 | import { 4 | State, 5 | GeometryStyle, 6 | PolygonStyle, 7 | LineStyle, 8 | EventType, 9 | EventListener, 10 | VisibleAnimationOpts, 11 | GrowthAnimationOpts, 12 | } from './interface'; 13 | import EventDispatcher from './events'; 14 | import cloneDeep from 'lodash.clonedeep'; 15 | // import merge from 'lodash.merge'; 16 | import * as Utils from './utils'; 17 | 18 | export default class Base { 19 | cesium: typeof CesiumTypeOnly; 20 | viewer: CesiumTypeOnly.Viewer; 21 | eventHandler: CesiumTypeOnly.ScreenSpaceEventHandler; 22 | polygonEntity: CesiumTypeOnly.Entity; 23 | geometryPoints: CesiumTypeOnly.Cartesian3[] = []; 24 | state: State = 'drawing'; 25 | controlPoints: CesiumTypeOnly.EntityCollection = []; 26 | controlPointsEventHandler: CesiumTypeOnly.ScreenSpaceEventHandler; 27 | lineEntity: CesiumTypeOnly.Entity; 28 | type!: 'polygon' | 'line'; 29 | freehand!: boolean; 30 | style: GeometryStyle | undefined; 31 | outlineEntity: CesiumTypeOnly.Entity; 32 | eventDispatcher: EventDispatcher; 33 | dragEventHandler: CesiumTypeOnly.ScreenSpaceEventHandler; 34 | entityId: string = ''; 35 | points: CesiumTypeOnly.Cartesian3[] = []; 36 | styleCache: GeometryStyle | undefined; 37 | minPointsForShape: number = 0; 38 | tempLineEntity: CesiumTypeOnly.Entity; 39 | 40 | constructor(cesium: CesiumTypeOnly, viewer: CesiumTypeOnly.Viewer, style?: GeometryStyle) { 41 | this.cesium = cesium; 42 | this.viewer = viewer; 43 | this.type = this.getType(); 44 | 45 | this.mergeStyle(style); 46 | this.cartesianToLnglat = this.cartesianToLnglat.bind(this); 47 | this.pixelToCartesian = this.pixelToCartesian.bind(this); 48 | this.eventDispatcher = new EventDispatcher(); 49 | // Disable default behavior for double-clicking on entities. 50 | viewer.trackedEntity = undefined; 51 | viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); 52 | 53 | this.onClick(); 54 | } 55 | 56 | mergeStyle(style: GeometryStyle | undefined) { 57 | if (this.type === 'polygon') { 58 | this.style = Object.assign( 59 | { 60 | material: new this.cesium.Color(), 61 | outlineMaterial: new this.cesium.Color(), 62 | outlineWidth: 2, 63 | }, 64 | style, 65 | ); 66 | } else if (this.type === 'line') { 67 | this.style = Object.assign( 68 | { 69 | material: new this.cesium.Color(), 70 | lineWidth: 2, 71 | }, 72 | style, 73 | ); 74 | } 75 | //Cache the initial settings to avoid modification of properties due to reference type assignment. 76 | this.styleCache = cloneDeep(this.style); 77 | } 78 | 79 | /** 80 | * The base class provides a method to change the state, and different logic is implemented based on the state. 81 | * The state is controlled by individual sub-components according to the actual situation. 82 | * @param state 83 | */ 84 | setState(state: State) { 85 | this.state = state; 86 | } 87 | 88 | getState(): State { 89 | return this.state; 90 | } 91 | 92 | /** 93 | * Bind a global click event that responds differently based on the state. When in the drawing state, 94 | * a click will add points for geometric shapes. During editing, selecting a drawn shape puts it in an 95 | * editable state. Clicking on empty space sets it to a static state. 96 | */ 97 | onClick() { 98 | this.eventHandler = new this.cesium.ScreenSpaceEventHandler(this.viewer.canvas); 99 | this.eventHandler.setInputAction((evt: any) => { 100 | const pickedObject = this.viewer.scene.pick(evt.position); 101 | const hitEntities = this.cesium.defined(pickedObject) && pickedObject.id instanceof this.cesium.Entity; 102 | let activeEntity = this.polygonEntity; 103 | if (this.type === 'line') { 104 | activeEntity = this.lineEntity; 105 | } 106 | 107 | if (this.state === 'drawing') { 108 | // In the drawing state, the points clicked are key nodes of the shape, and they are saved in this.points. 109 | const cartesian = this.pixelToCartesian(evt.position); 110 | const points = this.getPoints(); 111 | // If the click is outside the sphere, position information cannot be obtained. 112 | if (!cartesian) { 113 | return; 114 | } 115 | 116 | // "For non-freehand drawn shapes, validate that the distance between two consecutive clicks is greater than 10 meters 117 | if (!this.freehand && points.length > 0 && !this.checkDistance(cartesian, points[points.length - 1])) { 118 | return; 119 | } 120 | this.addPoint(cartesian); 121 | 122 | // Trigger 'drawStart' when the first point is being drawn. 123 | if (this.getPoints().length === 1) { 124 | this.eventDispatcher.dispatchEvent('drawStart'); 125 | } 126 | this.eventDispatcher.dispatchEvent('drawUpdate', cartesian); 127 | } else if (this.state === 'edit') { 128 | //In edit mode, exit the editing state and delete control points when clicking outside the currently edited shape. 129 | if (!hitEntities || activeEntity.id !== pickedObject.id.id) { 130 | this.setState('static'); 131 | this.removeControlPoints(); 132 | this.disableDrag(); 133 | // Trigger 'drawEnd' and return the geometry shape points when exiting the edit mode. 134 | this.eventDispatcher.dispatchEvent('editEnd', this.getPoints()); 135 | } 136 | } else if (this.state === 'static') { 137 | //When drawing multiple shapes, the click events for all shapes are triggered. Only when hitting a completed shape should it enter editing mode. 138 | if (hitEntities && activeEntity.id === pickedObject.id.id) { 139 | const pickedGraphics = this.type === 'line' ? pickedObject.id.polyline : pickedObject.id.polygon; 140 | if (this.cesium.defined(pickedGraphics)) { 141 | // Hit Geometry Shape. 142 | this.setState('edit'); 143 | this.addControlPoints(); 144 | this.draggable(); 145 | this.eventDispatcher.dispatchEvent('editStart'); 146 | } 147 | } 148 | } 149 | }, this.cesium.ScreenSpaceEventType.LEFT_CLICK); 150 | } 151 | 152 | onMouseMove() { 153 | this.eventHandler.setInputAction((evt: any) => { 154 | const points = this.getPoints(); 155 | const cartesian = this.pixelToCartesian(evt.endPosition); 156 | if (!cartesian) { 157 | return; 158 | } 159 | if (this.checkDistance(cartesian, points[points.length - 1])) { 160 | // Synchronize data to subclasses.If the distance is less than 10 meters, do not proceed 161 | this.updateMovingPoint(cartesian, points.length); 162 | } 163 | }, this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 164 | } 165 | 166 | onDoubleClick() { 167 | this.eventHandler.setInputAction((evt: any) => { 168 | if (this.state === 'drawing') { 169 | this.finishDrawing(); 170 | } 171 | }, this.cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); 172 | } 173 | 174 | /** 175 | * Check if the distance between two points is greater than 10 meters. 176 | */ 177 | checkDistance(cartesian1: CesiumTypeOnly.Cartesian3, cartesian2: CesiumTypeOnly.Cartesian3) { 178 | const distance = this.cesium.Cartesian3.distance(cartesian1, cartesian2); 179 | return distance > 10; 180 | } 181 | 182 | finishDrawing() { 183 | // Some polygons draw a separate line between the first two points before drawing the complete shape; 184 | // this line should be removed after drawing is complete. 185 | this.type === 'polygon' && this.lineEntity && this.viewer.entities.remove(this.lineEntity); 186 | 187 | this.removeMoveListener(); 188 | // Editable upon initial drawing completion. 189 | this.setState('edit'); 190 | this.addControlPoints(); 191 | this.draggable(); 192 | const entity = this.polygonEntity || this.lineEntity; 193 | this.entityId = entity.id; 194 | /** 195 | * "I've noticed that CallbackProperty can lead to significant performance issues. 196 | * After drawing multiple shapes, the map becomes noticeably laggy. Using methods 197 | * like requestAnimationFrame or setInterval doesn't provide a smooth way to display 198 | * shapes during the drawing process. As a temporary solution, I've set the hierarchy 199 | * or positions to static after drawing is complete. This addresses the performance 200 | * problem, but introduces a new issue: after setting the data to static, the shapes 201 | * redraw, resulting in a flicker. However, this seems to be a relatively reasonable 202 | * approach given the current circumstances." 203 | */ 204 | // TODO... 205 | // if (this.type === 'polygon') { 206 | // this.polygonEntity.polygon.hierarchy = new this.cesium.PolygonHierarchy(this.geometryPoints); 207 | // this.outlineEntity.polyline.positions = [...this.geometryPoints, this.geometryPoints[0]]; 208 | // } else if (this.type === 'line') { 209 | // this.lineEntity.polyline.positions = this.geometryPoints; 210 | // } 211 | 212 | this.eventDispatcher.dispatchEvent('drawEnd', this.getPoints()); 213 | } 214 | 215 | removeClickListener() { 216 | this.eventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_CLICK); 217 | } 218 | 219 | removeMoveListener() { 220 | this.eventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 221 | } 222 | 223 | removeDoubleClickListener() { 224 | this.eventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); 225 | } 226 | 227 | setGeometryPoints(geometryPoints: CesiumTypeOnly.Cartesian3[]) { 228 | this.geometryPoints = geometryPoints; 229 | } 230 | 231 | getGeometryPoints(): CesiumTypeOnly.Cartesian3[] { 232 | return this.geometryPoints; 233 | } 234 | 235 | drawPolygon() { 236 | const callback = () => { 237 | return new this.cesium.PolygonHierarchy(this.geometryPoints); 238 | }; 239 | if (!this.polygonEntity) { 240 | const style = this.style as PolygonStyle; 241 | this.polygonEntity = this.viewer.entities.add({ 242 | polygon: new this.cesium.PolygonGraphics({ 243 | hierarchy: new this.cesium.CallbackProperty(callback, false), 244 | show: true, 245 | material: style.material, 246 | }), 247 | }); 248 | 249 | // Due to limitations in PolygonGraphics outlining, a separate line style is drawn. 250 | this.outlineEntity = this.viewer.entities.add({ 251 | polyline: { 252 | positions: new this.cesium.CallbackProperty(() => { 253 | return [...this.geometryPoints, this.geometryPoints[0]]; 254 | }, false), 255 | width: style.outlineWidth, 256 | material: style.outlineMaterial, 257 | clampToGround: true, 258 | }, 259 | }); 260 | } 261 | } 262 | 263 | drawLine() { 264 | if (!this.lineEntity) { 265 | const style = this.style as LineStyle; 266 | this.lineEntity = this.addLineEntity(style); 267 | } 268 | } 269 | 270 | addTempLine() { 271 | if (!this.tempLineEntity) { 272 | // The line style between the first two points matches the outline style. 273 | const style = this.style as PolygonStyle; 274 | const lineStyle = { 275 | material: style.outlineMaterial, 276 | lineWidth: style.outlineWidth, 277 | }; 278 | this.tempLineEntity = this.addLineEntity(lineStyle); 279 | } 280 | } 281 | 282 | removeTempLine() { 283 | if (this.tempLineEntity) { 284 | this.viewer.entities.remove(this.tempLineEntity); 285 | } 286 | } 287 | 288 | addLineEntity(style: LineStyle) { 289 | const entity = this.viewer.entities.add({ 290 | polyline: { 291 | positions: new this.cesium.CallbackProperty(() => this.geometryPoints, false), 292 | width: style.lineWidth, 293 | material: style.material, 294 | clampToGround: true, 295 | }, 296 | }); 297 | return entity; 298 | } 299 | 300 | cartesianToLnglat(cartesian: CesiumTypeOnly.Cartesian3): [number, number] { 301 | const lnglat = this.viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian); 302 | const lat = this.cesium.Math.toDegrees(lnglat.latitude); 303 | const lng = this.cesium.Math.toDegrees(lnglat.longitude); 304 | return [lng, lat]; 305 | } 306 | 307 | pixelToCartesian(position: CesiumTypeOnly.Cartesian2): CesiumTypeOnly.Cartesian3 | undefined { 308 | const ray = this.viewer.camera.getPickRay(position); 309 | const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene); 310 | return cartesian; 311 | } 312 | 313 | /** 314 | * Display key points when creating a shape, allowing dragging of these points to edit and generate new shapes. 315 | */ 316 | addControlPoints() { 317 | const points = this.getPoints(); 318 | this.controlPoints = points.map((position) => { 319 | // return this.viewer.entities.add({ 320 | // position, 321 | // billboard: { 322 | // image: './src/assets/circle_red.png', 323 | // }, 324 | // }); 325 | 326 | return this.viewer.entities.add({ 327 | position, 328 | point: { 329 | pixelSize: 10, 330 | heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND, 331 | color: this.cesium.Color.RED, 332 | }, 333 | }); 334 | }); 335 | 336 | let isDragging = false; 337 | let draggedIcon: CesiumTypeOnly.Entity = null; 338 | let dragStartPosition: CesiumTypeOnly.Cartesian3; 339 | 340 | this.controlPointsEventHandler = new this.cesium.ScreenSpaceEventHandler(this.viewer.canvas); 341 | 342 | // Listen for left mouse button press events 343 | this.controlPointsEventHandler.setInputAction((clickEvent: any) => { 344 | const pickedObject = this.viewer.scene.pick(clickEvent.position); 345 | 346 | if (this.cesium.defined(pickedObject)) { 347 | for (let i = 0; i < this.controlPoints.length; i++) { 348 | if (pickedObject.id === this.controlPoints[i]) { 349 | isDragging = true; 350 | draggedIcon = this.controlPoints[i]; 351 | dragStartPosition = draggedIcon.position._value; 352 | //Save the index of dragged points for dynamic updates during movement 353 | draggedIcon.index = i; 354 | break; 355 | } 356 | } 357 | // Disable default camera interaction. 358 | this.viewer.scene.screenSpaceCameraController.enableRotate = false; 359 | } 360 | }, this.cesium.ScreenSpaceEventType.LEFT_DOWN); 361 | 362 | // Listen for mouse movement events 363 | this.controlPointsEventHandler.setInputAction((moveEvent: any) => { 364 | if (isDragging && draggedIcon) { 365 | const cartesian = this.viewer.camera.pickEllipsoid(moveEvent.endPosition, this.viewer.scene.globe.ellipsoid); 366 | if (cartesian) { 367 | draggedIcon.position.setValue(cartesian); 368 | this.updateDraggingPoint(cartesian, draggedIcon.index); 369 | } 370 | } 371 | }, this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 372 | 373 | // Listen for left mouse button release events 374 | this.controlPointsEventHandler.setInputAction(() => { 375 | // Trigger 'drawUpdate' when there is a change in coordinates before and after dragging. 376 | if (draggedIcon && !this.cesium.Cartesian3.equals(dragStartPosition, draggedIcon.position._value)) { 377 | this.eventDispatcher.dispatchEvent('drawUpdate', draggedIcon.position._value); 378 | } 379 | isDragging = false; 380 | draggedIcon = null; 381 | this.viewer.scene.screenSpaceCameraController.enableRotate = true; 382 | }, this.cesium.ScreenSpaceEventType.LEFT_UP); 383 | } 384 | 385 | removeControlPoints() { 386 | if (this.controlPoints.length > 0) { 387 | this.controlPoints.forEach((entity: CesiumTypeOnly.Entity) => { 388 | this.viewer.entities.remove(entity); 389 | }); 390 | this.controlPointsEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_DOWN); 391 | this.controlPointsEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 392 | this.controlPointsEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_UP); 393 | } 394 | } 395 | 396 | /** 397 | * Allow the entire shape to be dragged while in edit mode. 398 | */ 399 | draggable() { 400 | let dragging = false; 401 | let startPosition: CesiumTypeOnly.Cartesian3 | undefined; 402 | this.dragEventHandler = new this.cesium.ScreenSpaceEventHandler(this.viewer.canvas); 403 | this.dragEventHandler.setInputAction((event: any) => { 404 | const pickRay = this.viewer.scene.camera.getPickRay(event.position); 405 | if (pickRay) { 406 | const cartesian = this.viewer.scene.globe.pick(pickRay, this.viewer.scene); 407 | const pickedObject = this.viewer.scene.pick(event.position); 408 | if (this.cesium.defined(pickedObject) && pickedObject.id instanceof this.cesium.Entity) { 409 | const clickedEntity = pickedObject.id; 410 | if (this.isCurrentEntity(clickedEntity.id)) { 411 | //Clicking on the current instance's entity initiates drag logic. 412 | dragging = true; 413 | startPosition = cartesian; 414 | this.viewer.scene.screenSpaceCameraController.enableRotate = false; 415 | } 416 | } 417 | } 418 | }, this.cesium.ScreenSpaceEventType.LEFT_DOWN); 419 | 420 | this.dragEventHandler.setInputAction((event: any) => { 421 | if (dragging && startPosition) { 422 | // Retrieve the world coordinates of the current mouse position. 423 | const newPosition = this.pixelToCartesian(event.endPosition); 424 | if (newPosition) { 425 | // Calculate the displacement vector. 426 | const translation = this.cesium.Cartesian3.subtract(newPosition, startPosition, new this.cesium.Cartesian3()); 427 | const newPoints = this.geometryPoints.map((p) => { 428 | return this.cesium.Cartesian3.add(p, translation, new this.cesium.Cartesian3()); 429 | }); 430 | 431 | //Move all key points according to a vector. 432 | this.points = this.points.map((p) => { 433 | return this.cesium.Cartesian3.add(p, translation, new this.cesium.Cartesian3()); 434 | }); 435 | 436 | // Move control points in the same manner. 437 | this.controlPoints.map((p: CesiumTypeOnly.Entity) => { 438 | const position = p.position?.getValue(this.cesium.JulianDate.now()); 439 | const newPosition = this.cesium.Cartesian3.add(position, translation, new this.cesium.Cartesian3()); 440 | p.position?.setValue(newPosition); 441 | }); 442 | 443 | this.setGeometryPoints(newPoints); 444 | if (this.minPointsForShape === 4) { 445 | // 双箭头在整体被拖拽时,需要同步更新生长动画的插值点 446 | this.curveControlPointLeft = this.cesium.Cartesian3.add(this.curveControlPointLeft, translation, new this.cesium.Cartesian3()); 447 | this.curveControlPointRight = this.cesium.Cartesian3.add(this.curveControlPointRight, translation, new this.cesium.Cartesian3()); 448 | } 449 | startPosition = newPosition; 450 | } 451 | } else { 452 | const pickRay = this.viewer.scene.camera.getPickRay(event.endPosition); 453 | if (pickRay) { 454 | const pickedObject = this.viewer.scene.pick(event.endPosition); 455 | if (this.cesium.defined(pickedObject) && pickedObject.id instanceof this.cesium.Entity) { 456 | const clickedEntity = pickedObject.id; 457 | // TODO 绘制的图形,需要特殊id标识,可在创建entity时指定id 458 | if (this.isCurrentEntity(clickedEntity.id)) { 459 | this.viewer.scene.canvas.style.cursor = 'move'; 460 | } else { 461 | this.viewer.scene.canvas.style.cursor = 'default'; 462 | } 463 | } else { 464 | this.viewer.scene.canvas.style.cursor = 'default'; 465 | } 466 | } 467 | } 468 | }, this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 469 | 470 | // Listen for the mouse release event to end dragging. 471 | this.dragEventHandler.setInputAction(() => { 472 | dragging = false; 473 | startPosition = undefined; 474 | this.viewer.scene.screenSpaceCameraController.enableRotate = true; 475 | }, this.cesium.ScreenSpaceEventType.LEFT_UP); 476 | } 477 | 478 | // Finish editing, disable dragging." 479 | disableDrag() { 480 | this.dragEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_DOWN); 481 | this.dragEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.MOUSE_MOVE); 482 | this.dragEventHandler.removeInputAction(this.cesium.ScreenSpaceEventType.LEFT_UP); 483 | } 484 | 485 | show(opts: VisibleAnimationOpts) { 486 | if (opts) { 487 | const { duration, delay, callback } = opts; 488 | this.showWithAnimation(duration, delay, callback); 489 | return; 490 | } else { 491 | this.showWithAnimation(0, 0); 492 | } 493 | } 494 | 495 | hide(opts: VisibleAnimationOpts) { 496 | if (opts) { 497 | const { duration, delay, callback } = opts; 498 | this.hideWithAnimation(duration, delay, callback); 499 | return; 500 | } else { 501 | this.hideWithAnimation(0, 0); 502 | } 503 | } 504 | 505 | showWithAnimation(duration: number = 2000, delay: number = 0, callback?: () => void) { 506 | if (this.state !== 'hidden') { 507 | //If not in a static state or already displayed, do not process. 508 | return; 509 | } 510 | this.setState('static'); 511 | if (this.type === 'polygon') { 512 | let alpha = 0.3; 513 | const material = this.styleCache.material; 514 | if (material.image) { 515 | // With Texture 516 | alpha = material.color.getValue().alpha; 517 | } else { 518 | alpha = material.alpha; 519 | } 520 | 521 | this.animateOpacity(this.polygonEntity, alpha, duration, delay, callback, this.state); 522 | const outlineAlpha = this.styleCache?.outlineMaterial?.alpha; 523 | this.animateOpacity(this.outlineEntity, outlineAlpha || 1.0, duration, delay, undefined, this.state); 524 | } else if (this.type === 'line') { 525 | const material = this.styleCache.material; 526 | let alpha = 1.0; 527 | if (material.image) { 528 | // With Texture 529 | alpha = material.color.alpha; 530 | } else if (material.dashLength) { 531 | // Dashed Line 532 | const color = material.color.getValue(); 533 | alpha = color.alpha; 534 | } else { 535 | // Solid Color 536 | alpha = this.styleCache?.material?.alpha; 537 | } 538 | this.animateOpacity(this.lineEntity, alpha, duration, delay, callback, this.state); 539 | } 540 | if (duration != 0) { 541 | this.setState('animating'); 542 | } 543 | } 544 | 545 | hideWithAnimation(duration: number = 2000, delay: number = 0, callback?: () => void) { 546 | if (this.state === 'hidden' || this.state != 'static') { 547 | return; 548 | } 549 | this.setState('hidden'); 550 | if (this.type === 'polygon') { 551 | this.animateOpacity(this.polygonEntity, 0.0, duration, delay, callback, this.state); 552 | this.animateOpacity(this.outlineEntity, 0.0, duration, delay, undefined, this.state); 553 | } else if (this.type === 'line') { 554 | this.animateOpacity(this.lineEntity, 0.0, duration, delay, callback, this.state); 555 | } 556 | // if (this.state == 'edit') { 557 | // this.controlPoints.forEach(p => { 558 | // this.animateOpacity(p, 0.0, duration, delay, undefined, this.state); 559 | // }); 560 | // } 561 | if (duration != 0) { 562 | this.setState('animating'); 563 | } 564 | } 565 | 566 | animateOpacity( 567 | entity: CesiumTypeOnly.Entity, 568 | targetAlpha: number, 569 | duration: number, 570 | delay: number, 571 | callback?: () => void, 572 | state?: State, 573 | ): void { 574 | setTimeout(() => { 575 | const graphics = entity.polygon || entity.polyline || entity.billboard; 576 | let startAlpha: number; 577 | let material = graphics.material; 578 | if (material) { 579 | if (material.image && material.color.alpha !== undefined) { 580 | // Texture material, setting the alpha channel in the color of the custom ImageFlowMaterialProperty. 581 | startAlpha = material.color.alpha; 582 | } else { 583 | startAlpha = material.color.getValue().alpha; 584 | } 585 | } else { 586 | // billbord 587 | const color = graphics.color.getValue(); 588 | startAlpha = color.alpha; 589 | } 590 | 591 | let startTime = 0; 592 | 593 | const animate = (currentTime: number) => { 594 | if (!startTime) { 595 | startTime = currentTime; 596 | } 597 | const elapsedTime = currentTime - startTime; 598 | 599 | if (elapsedTime < duration) { 600 | const deltalpha = (elapsedTime / duration) * (targetAlpha - startAlpha); 601 | const newAlpha = startAlpha + deltalpha; 602 | 603 | if (material) { 604 | if (material.image && material.color.alpha !== undefined) { 605 | // Texture Material 606 | material.color.alpha = newAlpha; 607 | } else { 608 | // Solid Color 609 | const newColor = material.color.getValue().withAlpha(newAlpha); 610 | material.color.setValue(newColor); 611 | } 612 | } else { 613 | // billbord 614 | const color = graphics.color.getValue(); 615 | const newColor = color.withAlpha(newAlpha); 616 | graphics.color.setValue(newColor); 617 | } 618 | 619 | requestAnimationFrame(animate); 620 | } else { 621 | // Animation Ended 622 | callback && callback(); 623 | const restoredState = state ? state : 'static'; 624 | 625 | // if (targetAlpha === 0) { 626 | // this.setState('hidden'); 627 | // } 628 | 629 | // if (duration == 0) { 630 | // this.setState('drawing'); 631 | if (material) { 632 | if (material.image && material.color.alpha !== undefined) { 633 | // Texture Material 634 | material.color.alpha = targetAlpha; 635 | } else { 636 | // Solid Color 637 | const newColor = material.color.getValue().withAlpha(targetAlpha); 638 | material.color.setValue(newColor); 639 | } 640 | } else { 641 | // billbord 642 | const color = graphics.color.getValue(); 643 | const newColor = color.withAlpha(targetAlpha); 644 | graphics.color.setValue(newColor); 645 | } 646 | requestAnimationFrame(() => { 647 | this.setState(restoredState); 648 | }); 649 | // } else { 650 | // this.setState(restoredState); 651 | // } 652 | } 653 | }; 654 | 655 | requestAnimationFrame(animate); 656 | }, delay); 657 | } 658 | 659 | startGrowthAnimation(opts: GrowthAnimationOpts) { 660 | const { duration = 2000, delay = 0, callback } = opts || {}; 661 | if (this.state === 'hidden' || this.state != 'static') { 662 | return; 663 | } 664 | if (!this.minPointsForShape) { 665 | console.warn('Growth animation is not supported for this type of shape'); 666 | return; 667 | } 668 | this.setState('animating'); 669 | if (this.minPointsForShape === 4) { 670 | // For double arrows, special handling is required. 671 | this.doubleArrowGrowthAnimation(duration, delay, callback); 672 | return; 673 | } 674 | setTimeout(() => { 675 | this.hideWithAnimation(0, 0, undefined); 676 | const points = this.getPoints(); 677 | 678 | let segmentDuration = 0; 679 | if (this.minPointsForShape === 2) { 680 | segmentDuration = duration / (points.length - 1); 681 | } else { 682 | segmentDuration = duration / (points.length - 2); 683 | } 684 | 685 | let startTime = Date.now(); 686 | let movingPointIndex = 0; 687 | this.viewer.clock.shouldAnimate = true; 688 | 689 | const frameListener = (clock) => { 690 | const currentTime = Date.now(); 691 | const elapsedTime = currentTime - startTime; 692 | if (elapsedTime >= duration) { 693 | // Animation ends 694 | callback && callback(); 695 | startTime = 0; 696 | this.viewer.clock.shouldAnimate = false; 697 | this.viewer.clock.onTick.removeEventListener(frameListener); 698 | this.setState('static'); 699 | return; 700 | } 701 | 702 | const currentSegment = Math.floor(elapsedTime / segmentDuration); 703 | let startPoint; 704 | 705 | if (this.minPointsForShape === 2) { 706 | movingPointIndex = currentSegment + 1; 707 | } else { 708 | movingPointIndex = currentSegment + 2; 709 | } 710 | startPoint = points[movingPointIndex - 1]; 711 | if (currentSegment == 0 && this.minPointsForShape === 3) { 712 | // The face-arrow determined by three points, with the animation starting from the midpoint of the line connecting the first two points. 713 | startPoint = this.cesium.Cartesian3.midpoint(points[0], points[1], new this.cesium.Cartesian3()); 714 | } 715 | let endPoint = points[movingPointIndex]; 716 | // To dynamically add points between the startPoint and endPoint, consistent with the initial drawing logic, 717 | // update the point at index movingPointIndex in the points array with the newPosition, 718 | // generate the arrow, and execute the animation. 719 | const t = (elapsedTime - currentSegment * segmentDuration) / segmentDuration; 720 | const newPosition = this.cesium.Cartesian3.lerp(startPoint, endPoint, t, new this.cesium.Cartesian3()); 721 | const tempPoints = points.slice(0, movingPointIndex + 1); 722 | tempPoints[tempPoints.length - 1] = newPosition; 723 | const geometryPoints = this.createGraphic(tempPoints); 724 | this.setGeometryPoints(geometryPoints); 725 | this.showWithAnimation(0, 0, undefined); 726 | }; 727 | this.viewer.clock.onTick.addEventListener(frameListener); 728 | }, delay); 729 | } 730 | 731 | private doubleArrowGrowthAnimation(duration: number = 2000, delay: number = 0, callback?: Function) { 732 | setTimeout(() => { 733 | this.hideWithAnimation(0, 0, undefined); 734 | const points = this.getPoints(); 735 | let startTime = Date.now(); 736 | this.viewer.clock.shouldAnimate = true; 737 | 738 | const frameListener = (clock) => { 739 | const currentTime = Date.now(); 740 | const elapsedTime = currentTime - startTime; 741 | if (elapsedTime >= duration) { 742 | // Animation ends 743 | callback && callback(); 744 | startTime = 0; 745 | this.viewer.clock.shouldAnimate = false; 746 | this.viewer.clock.onTick.removeEventListener(frameListener); 747 | this.setState('static'); 748 | return; 749 | } 750 | 751 | // Utils.isClockWise(pnt1, pnt2, pnt3) 752 | const midPoint = this.cesium.Cartesian3.midpoint(points[0], points[1], new this.cesium.Cartesian3()); 753 | 754 | const startPointLeft = this.cesium.Cartesian3.midpoint(points[0], midPoint, new this.cesium.Cartesian3()); 755 | 756 | const startPointRight = this.cesium.Cartesian3.midpoint(midPoint, points[1], new this.cesium.Cartesian3()); 757 | let endPointLeft = points[3]; 758 | let endPointRight = points[2]; 759 | const t = elapsedTime / duration; 760 | const controlPoint = this.getBezierControlPointforGrowthAnimation(); 761 | let curveControlPointsLeft = [startPointLeft, controlPoint.left, endPointLeft]; 762 | let curveControlPointsRight = [startPointRight, controlPoint.right, endPointRight]; 763 | const newPositionLeft = this.getNewPosition(curveControlPointsLeft, t); 764 | const newPositionRight = this.getNewPosition(curveControlPointsRight, t); 765 | 766 | // Assist in viewing exercise routes 767 | // this.viewer.entities.add({ 768 | // position: newPositionLeft, 769 | // point: { 770 | // pixelSize: 4, 771 | // heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND, 772 | // color: this.cesium.Color.RED, 773 | // }, 774 | // }); 775 | // this.viewer.entities.add({ 776 | // position: newPositionRight, 777 | // point: { 778 | // pixelSize: 4, 779 | // heightReference: this.cesium.HeightReference.CLAMP_TO_GROUND, 780 | // color: this.cesium.Color.RED, 781 | // }, 782 | // }); 783 | const tempPoints = [...points]; 784 | tempPoints[2] = newPositionRight; 785 | tempPoints[3] = newPositionLeft; 786 | const geometryPoints = this.createGraphic(tempPoints); 787 | this.setGeometryPoints(geometryPoints); 788 | this.showWithAnimation(0, 0, undefined); 789 | }; 790 | this.viewer.clock.onTick.addEventListener(frameListener); 791 | }, delay); 792 | } 793 | 794 | private getNewPosition(curveControlPoints, t) { 795 | curveControlPoints = curveControlPoints.map((item) => { 796 | return this.cartesianToLnglat(item); 797 | }); 798 | let curvePoints = Utils.getCurvePoints(0.3, curveControlPoints); 799 | curvePoints = curvePoints.map((p) => { 800 | return this.cesium.Cartesian3.fromDegrees(p[0], p[1]); 801 | }); 802 | 803 | let newPosition = this.interpolateAlongCurve(curvePoints, t); 804 | return newPosition; 805 | } 806 | 807 | private interpolateAlongCurve(curvePoints, t) { 808 | const numPoints = curvePoints.length - 1; 809 | const index = Math.floor(t * numPoints); 810 | const tSegment = t * numPoints - index; 811 | const startPoint = curvePoints[index]; 812 | const endPoint = curvePoints[index + 1]; 813 | const x = startPoint.x + (endPoint.x - startPoint.x) * tSegment; 814 | const y = startPoint.y + (endPoint.y - startPoint.y) * tSegment; 815 | const z = startPoint.z + (endPoint.z - startPoint.z) * tSegment; 816 | 817 | return new this.cesium.Cartesian3(x, y, z); 818 | } 819 | 820 | remove() { 821 | if (this.type === 'polygon') { 822 | this.viewer.entities.remove(this.polygonEntity); 823 | this.viewer.entities.remove(this.outlineEntity); 824 | this.polygonEntity = null; 825 | this.outlineEntity = null; 826 | this.lineEntity = null; 827 | } else if (this.type === 'line') { 828 | this.viewer.entities.remove(this.lineEntity); 829 | } 830 | this.removeClickListener(); 831 | this.removeMoveListener(); 832 | this.removeDoubleClickListener(); 833 | this.removeControlPoints(); 834 | } 835 | 836 | on(eventType: EventType, listener: EventListener) { 837 | this.eventDispatcher.on(eventType, listener); 838 | } 839 | 840 | off(eventType: EventType, listener: EventListener) { 841 | this.eventDispatcher.off(eventType, listener); 842 | } 843 | 844 | isCurrentEntity(id: string) { 845 | // return this.entityId === `CesiumPlot-${id}`; 846 | return this.entityId === id; 847 | } 848 | 849 | addPoint(cartesian: CesiumTypeOnly.Cartesian3) { 850 | //Abstract method that must be implemented by subclasses. 851 | } 852 | 853 | getPoints(): CesiumTypeOnly.Cartesian3[] { 854 | //Abstract method that must be implemented by subclasses. 855 | return [new this.cesium.Cartesian3()]; 856 | } 857 | 858 | updateMovingPoint(cartesian: CesiumTypeOnly.Cartesian3, index?: number) { 859 | //Abstract method that must be implemented by subclasses. 860 | } 861 | 862 | updateDraggingPoint(cartesian: CesiumTypeOnly.Cartesian3, index: number) { 863 | //Abstract method that must be implemented by subclasses. 864 | } 865 | 866 | getType(): 'polygon' | 'line' { 867 | return 'polygon'; 868 | //Abstract method that must be implemented by subclasses. 869 | } 870 | 871 | createGraphic(points: CesiumTypeOnly.Cartesian3[]): CesiumTypeOnly.Cartesian3[] { 872 | //Abstract method that must be implemented by subclasses. 873 | return points; 874 | } 875 | } 876 | --------------------------------------------------------------------------------