You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
11 KiB

1 month ago
  1. import {
  2. Cartesian3,
  3. Cartographic,
  4. Camera,
  5. Color,
  6. CallbackProperty,
  7. HeightReference,
  8. LabelStyle,
  9. VerticalOrigin,
  10. PerspectiveFrustum,
  11. DebugCameraPrimitive,
  12. Entity,
  13. Cartesian2,
  14. Math as cesiumMath
  15. } from 'cesium'
  16. import { Wayline } from './wayline'
  17. import { viewer } from './basemap'
  18. import * as egm96 from 'egm96-universal'
  19. export class WayPoint {
  20. position: Cartesian3
  21. wayline: Wayline
  22. index: number
  23. asl: number
  24. alt: number
  25. fov: number
  26. selected!: boolean
  27. heightLabel!: Entity
  28. wayPointEntity!: Entity
  29. orientation: { heading: number; pitch: number; roll: number }
  30. debugCameraPrimitive: any
  31. distanceLabel1: any
  32. distanceLabel2: any
  33. constructor(position: Cartesian3, wayline: Wayline, index: number) {
  34. this.position = position
  35. this.wayline = wayline
  36. this.index = index
  37. const cartographic = Cartographic.fromCartesian(position)
  38. this.alt = cartographic.height
  39. const height = egm96.meanSeaLevel(cesiumMath.toDegrees(cartographic.latitude), cesiumMath.toDegrees(cartographic.longitude))
  40. this.asl = this.alt - height
  41. this.createWaypointEntity()
  42. const minimapViewer = this.wayline.minimapViewer
  43. this.fov = minimapViewer.camera.frustum.fov
  44. this.orientation = {
  45. heading: minimapViewer.camera.heading,
  46. pitch: minimapViewer.camera.pitch,
  47. roll: minimapViewer.camera.roll
  48. }
  49. }
  50. private createWaypointEntity(): void {
  51. const { position, index } = this
  52. const url = generateNumberSvgDataUrl(index + 1)
  53. this.wayPointEntity = viewer.entities.add({
  54. position,
  55. wayPoint: this,
  56. point: {
  57. pixelSize: 10,
  58. color: Color.RED,
  59. heightReference: HeightReference.NONE
  60. },
  61. polyline: {
  62. positions: new CallbackProperty(() => {
  63. const position = this.position
  64. const cartographic = Cartographic.fromCartesian(position)
  65. const bottom = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0)
  66. return [position, bottom]
  67. }, false),
  68. width: 2,
  69. material: Color.WHITE
  70. },
  71. billboard: {
  72. image: url,
  73. scale: 1.0,
  74. verticalOrigin: VerticalOrigin.BOTTOM,
  75. disableDepthTestDistance: Number.POSITIVE_INFINITY
  76. }
  77. })
  78. }
  79. updatePosition(position: Cartesian3): void {
  80. const pointList = this.wayline.pointList
  81. this.position = position
  82. this.wayPointEntity.position = position
  83. this.heightLabel.position = position
  84. this.debugCameraPrimitive._camera.position = position
  85. const text = `ASL: ${this.asl.toFixed(1)} m\nHAE: ${this.alt.toFixed(1)} m`
  86. this.heightLabel.label.text = text
  87. const index = this.index
  88. const prev = pointList[index - 1]
  89. const next = pointList[index + 1]
  90. if (prev) {
  91. const distance = Cartesian3.distance(
  92. prev.position,
  93. position
  94. )
  95. const midpoint = Cartesian3.lerp(prev.position, position, 0.5, new Cartesian3())
  96. this.distanceLabel1.position = midpoint
  97. this.distanceLabel1.label.text = distance.toFixed(1) + 'm'
  98. }
  99. if (next) {
  100. const distance = Cartesian3.distance(
  101. position,
  102. next.position
  103. )
  104. const midpoint = Cartesian3.lerp(position, next.position, 0.5, new Cartesian3())
  105. this.distanceLabel2.position = midpoint
  106. this.distanceLabel2.label.text = distance.toFixed(1) + 'm'
  107. }
  108. }
  109. select(): void {
  110. this.selected = true
  111. this.wayPointEntity.billboard.image = generateHighlightedNumberSvgDataUrl(this.index + 1)
  112. this.showDistanceLabels()
  113. this.showHeightLabel()
  114. const camera = new Camera(viewer.scene)
  115. camera.frustum = new PerspectiveFrustum({
  116. fov: this.fov,
  117. aspectRatio: 4 / 3,
  118. near: 1,
  119. far: 100
  120. })
  121. camera.setView({
  122. destination: this.position,
  123. orientation: this.orientation
  124. })
  125. const minimapViewer = this.wayline.minimapViewer
  126. minimapViewer.camera.setView({
  127. destination: this.position,
  128. orientation: this.orientation
  129. })
  130. minimapViewer.camera.frustum.fov = this.fov
  131. this.debugCameraPrimitive = new DebugCameraPrimitive({
  132. camera,
  133. color: Color.YELLOW
  134. })
  135. viewer.scene.primitives.add(this.debugCameraPrimitive)
  136. }
  137. unselect(): void {
  138. this.selected = false
  139. this.wayPointEntity.billboard.image = generateNumberSvgDataUrl(this.index + 1)
  140. viewer.entities.remove(this.distanceLabel0)
  141. viewer.entities.remove(this.distanceLabel1)
  142. viewer.entities.remove(this.distanceLabel2)
  143. viewer.entities.remove(this.heightLabel)
  144. viewer.scene.primitives.remove(this.debugCameraPrimitive)
  145. }
  146. distanceLabel0(distanceLabel0: any) {
  147. throw new Error('Method not implemented.')
  148. }
  149. delete(): void {
  150. const pointList = this.wayline.pointList
  151. viewer.entities.remove(this.wayPointEntity)
  152. viewer.scene.primitives.remove(this.debugCameraPrimitive)
  153. pointList.splice(this.index, 1)
  154. for (let i = this.index; i < pointList.length; i++) {
  155. pointList[i].wayPointEntity.billboard.image = generateNumberSvgDataUrl(i + 1)
  156. pointList[i].index = i
  157. }
  158. }
  159. private showHeightLabel(): void {
  160. const { position, alt, asl } = this
  161. const text = `ASL: ${asl.toFixed(1)} m\nHAE: ${alt.toFixed(1)} m`
  162. this.heightLabel = viewer.entities.add({
  163. position,
  164. label: {
  165. text,
  166. font: "14px sans-serif",
  167. fillColor: Color.WHITE,
  168. outlineColor: Color.BLACK,
  169. outlineWidth: 2,
  170. style: LabelStyle.FILL_AND_OUTLINE,
  171. verticalOrigin: VerticalOrigin.BOTTOM,
  172. heightReference: HeightReference.NONE,
  173. showBackground: true,
  174. backgroundColor: Color.BLACK.withAlpha(0.5),
  175. pixelOffset: new Cartesian2(0, -30)
  176. }
  177. })
  178. }
  179. private showDistanceLabels(): void {
  180. const index = this.index
  181. const pointList = this.wayline.pointList
  182. const current = pointList[index]
  183. const prev = pointList[index - 1]
  184. const next = pointList[index + 1]
  185. if (prev) {
  186. this.distanceLabel1 = this.createMidpointLabel(prev.position, current.position)
  187. } else {
  188. this.distanceLabel0 = this.createMidpointLabel(this.wayline.takeOffPoint, this.wayline.top)
  189. this.distanceLabel1 = this.createMidpointLabel(this.wayline.top, current.position)
  190. }
  191. if (next) {
  192. this.distanceLabel2 = this.createMidpointLabel(current.position, next.position)
  193. }
  194. }
  195. private createMidpointLabel(point1: Cartesian3, point2: Cartesian3): Entity {
  196. const midpoint = Cartesian3.lerp(point1, point2, 0.5, new Cartesian3())
  197. const distance = Cartesian3.distance(
  198. point1,
  199. point2
  200. )
  201. return viewer.entities.add({
  202. position: midpoint,
  203. label: {
  204. text: `${distance.toFixed(1)}m`,
  205. font: "14px sans-serif",
  206. fillColor: Color.WHITE,
  207. outlineColor: Color.BLACK,
  208. outlineWidth: 2,
  209. style: LabelStyle.FILL_AND_OUTLINE,
  210. verticalOrigin: VerticalOrigin.BOTTOM,
  211. heightReference: HeightReference.NONE,
  212. showBackground: true,
  213. backgroundColor: Color.BLACK.withAlpha(0.5),
  214. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  215. eyeOffset: new Cartesian3(0.0, 0.0, -10.0),
  216. pixelOffset: new Cartesian2(0, -10)
  217. }
  218. })
  219. }
  220. destroy(): void {
  221. viewer.entities.remove(this.wayPointEntity)
  222. }
  223. }
  224. function generateNumberSvgDataUrl(number: number): string {
  225. const svg = `
  226. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 26" width="32" height="26">
  227. <g fill="none" fill-rule="evenodd">
  228. <path fill="#00D690" d="M16.8320503 24.75192456L30.9635332 3.5547002c.3063525-.4595287.1821786-1.080398-.2773501-1.3867505C30.5219156 2.058438 30.3289079 2 30.1314829 2H1.86851709c-.55228475 0-1 .4477153-1 1 0 .197425.05843803.3904327.16794971.5547002l14.1314829 21.19722436c.3063525.45952869.9272218.58370256 1.3867505.2773501.1098523-.07323486.2041152-.16749781.2773501-.2773501z"/>
  229. <text fill="#FFF" font-size="16" font-weight="500">
  230. <tspan x="50%" y="50%" dy=".25em" text-anchor="middle">${number}</tspan>
  231. </text>
  232. </g>
  233. </svg>
  234. `
  235. const encoded = encodeURIComponent(svg)
  236. .replace(/'/g, '%27')
  237. .replace(/"/g, '%22')
  238. return `data:image/svg+xml,${encoded}`
  239. }
  240. function generateHighlightedNumberSvgDataUrl(number: number): string {
  241. const svg = `
  242. <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
  243. viewBox="0 0 42 35" width="42" height="35">
  244. <defs>
  245. <filter id="a" width="150.8%" height="171.7%" x="-25.4%" y="-34.2%" filterUnits="objectBoundingBox">
  246. <feMorphology in="SourceAlpha" operator="dilate" radius="1" result="shadowSpreadOuter1"/>
  247. <feOffset in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
  248. <feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="2"/>
  249. <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/>
  250. <feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0.039215686 0 0 0 0 0.933333333 0 0 0 0 0.545098039 0 0 0 1 0"/>
  251. </filter>
  252. <path id="b" d="M16.832 3.248l14.132 21.197A1 1 0 0130.13 26H1.87a1 1 0 01-.833-1.555L15.168 3.248a1 1 0 011.664 0z"/>
  253. </defs>
  254. <g fill="none" fill-rule="evenodd">
  255. <g transform="matrix(1 0 0 -1 5 32)">
  256. <use fill="#000" filter="url(#a)" xlink:href="#b"/>
  257. <path fill="#00D690" stroke="#FFF" stroke-linejoin="round" stroke-width="2"
  258. d="M15.955 3.871L30.13 25H1.87L15.955 3.871z"/>
  259. </g>
  260. <text fill="#FFF" font-size="14" font-weight="500">
  261. <tspan x="50%" y="50%" dy=".1em" text-anchor="middle">${number}</tspan>
  262. </text>
  263. </g>
  264. </svg>
  265. `
  266. const encoded = encodeURIComponent(svg)
  267. .replace(/'/g, '%27')
  268. .replace(/"/g, '%22')
  269. return `data:image/svg+xml,${encoded}`
  270. }