智慧水务管理系统 - 精河县供水工程综合管理平台

GpxDataSource.js 30KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. import Cartesian2 from "../Core/Cartesian2.js";
  2. import Cartesian3 from "../Core/Cartesian3.js";
  3. import ClockRange from "../Core/ClockRange.js";
  4. import ClockStep from "../Core/ClockStep.js";
  5. import Color from "../Core/Color.js";
  6. import createGuid from "../Core/createGuid.js";
  7. import Frozen from "../Core/Frozen.js";
  8. import defined from "../Core/defined.js";
  9. import DeveloperError from "../Core/DeveloperError.js";
  10. import Event from "../Core/Event.js";
  11. import Iso8601 from "../Core/Iso8601.js";
  12. import JulianDate from "../Core/JulianDate.js";
  13. import NearFarScalar from "../Core/NearFarScalar.js";
  14. import PinBuilder from "../Core/PinBuilder.js";
  15. import Resource from "../Core/Resource.js";
  16. import RuntimeError from "../Core/RuntimeError.js";
  17. import TimeInterval from "../Core/TimeInterval.js";
  18. import TimeIntervalCollection from "../Core/TimeIntervalCollection.js";
  19. import HeightReference from "../Scene/HeightReference.js";
  20. import HorizontalOrigin from "../Scene/HorizontalOrigin.js";
  21. import LabelStyle from "../Scene/LabelStyle.js";
  22. import VerticalOrigin from "../Scene/VerticalOrigin.js";
  23. import Autolinker from "autolinker";
  24. import BillboardGraphics from "./BillboardGraphics.js";
  25. import ConstantProperty from "./ConstantProperty.js";
  26. import DataSource from "./DataSource.js";
  27. import DataSourceClock from "./DataSourceClock.js";
  28. import EntityCluster from "./EntityCluster.js";
  29. import EntityCollection from "./EntityCollection.js";
  30. import LabelGraphics from "./LabelGraphics.js";
  31. import PolylineGraphics from "./PolylineGraphics.js";
  32. import PolylineOutlineMaterialProperty from "./PolylineOutlineMaterialProperty.js";
  33. import SampledPositionProperty from "./SampledPositionProperty.js";
  34. let parser;
  35. if (typeof DOMParser !== "undefined") {
  36. parser = new DOMParser();
  37. }
  38. const autolinker = new Autolinker({
  39. stripPrefix: false,
  40. email: false,
  41. replaceFn: function (linker, match) {
  42. //Prevent matching of non-explicit urls.
  43. //i.e. foo.id won't match but http://foo.id will
  44. return match.urlMatchType === "scheme" || match.urlMatchType === "www";
  45. },
  46. });
  47. const BILLBOARD_SIZE = 32;
  48. const BILLBOARD_NEAR_DISTANCE = 2414016;
  49. const BILLBOARD_NEAR_RATIO = 1.0;
  50. const BILLBOARD_FAR_DISTANCE = 1.6093e7;
  51. const BILLBOARD_FAR_RATIO = 0.1;
  52. const gpxNamespaces = [null, undefined, "http://www.topografix.com/GPX/1/1"];
  53. const namespaces = {
  54. gpx: gpxNamespaces,
  55. };
  56. function readBlobAsText(blob) {
  57. return new Promise((resolve, reject) => {
  58. const reader = new FileReader();
  59. reader.addEventListener("load", function () {
  60. resolve(reader.result);
  61. });
  62. reader.addEventListener("error", function () {
  63. reject(reader.error);
  64. });
  65. reader.readAsText(blob);
  66. });
  67. }
  68. function getOrCreateEntity(node, entityCollection) {
  69. let id = queryStringAttribute(node, "id");
  70. id = defined(id) ? id : createGuid();
  71. const entity = entityCollection.getOrCreateEntity(id);
  72. return entity;
  73. }
  74. function readCoordinateFromNode(node) {
  75. const longitude = queryNumericAttribute(node, "lon");
  76. const latitude = queryNumericAttribute(node, "lat");
  77. const elevation = queryNumericValue(node, "ele", namespaces.gpx);
  78. return Cartesian3.fromDegrees(longitude, latitude, elevation);
  79. }
  80. function queryNumericAttribute(node, attributeName) {
  81. if (!defined(node)) {
  82. return undefined;
  83. }
  84. const value = node.getAttribute(attributeName);
  85. if (value !== null) {
  86. const result = parseFloat(value);
  87. return !isNaN(result) ? result : undefined;
  88. }
  89. return undefined;
  90. }
  91. function queryStringAttribute(node, attributeName) {
  92. if (!defined(node)) {
  93. return undefined;
  94. }
  95. const value = node.getAttribute(attributeName);
  96. return value !== null ? value : undefined;
  97. }
  98. function queryFirstNode(node, tagName, namespace) {
  99. if (!defined(node)) {
  100. return undefined;
  101. }
  102. const childNodes = node.childNodes;
  103. const length = childNodes.length;
  104. for (let q = 0; q < length; q++) {
  105. const child = childNodes[q];
  106. if (
  107. child.localName === tagName &&
  108. namespace.indexOf(child.namespaceURI) !== -1
  109. ) {
  110. return child;
  111. }
  112. }
  113. return undefined;
  114. }
  115. function queryNodes(node, tagName, namespace) {
  116. if (!defined(node)) {
  117. return undefined;
  118. }
  119. const result = [];
  120. const childNodes = node.getElementsByTagName(tagName);
  121. const length = childNodes.length;
  122. for (let q = 0; q < length; q++) {
  123. const child = childNodes[q];
  124. if (
  125. child.localName === tagName &&
  126. namespace.indexOf(child.namespaceURI) !== -1
  127. ) {
  128. result.push(child);
  129. }
  130. }
  131. return result;
  132. }
  133. function queryNumericValue(node, tagName, namespace) {
  134. const resultNode = queryFirstNode(node, tagName, namespace);
  135. if (defined(resultNode)) {
  136. const result = parseFloat(resultNode.textContent);
  137. return !isNaN(result) ? result : undefined;
  138. }
  139. return undefined;
  140. }
  141. function queryStringValue(node, tagName, namespace) {
  142. const result = queryFirstNode(node, tagName, namespace);
  143. if (defined(result)) {
  144. return result.textContent.trim();
  145. }
  146. return undefined;
  147. }
  148. function createDefaultBillboard(image) {
  149. const billboard = new BillboardGraphics();
  150. billboard.width = BILLBOARD_SIZE;
  151. billboard.height = BILLBOARD_SIZE;
  152. billboard.scaleByDistance = new NearFarScalar(
  153. BILLBOARD_NEAR_DISTANCE,
  154. BILLBOARD_NEAR_RATIO,
  155. BILLBOARD_FAR_DISTANCE,
  156. BILLBOARD_FAR_RATIO,
  157. );
  158. billboard.pixelOffsetScaleByDistance = new NearFarScalar(
  159. BILLBOARD_NEAR_DISTANCE,
  160. BILLBOARD_NEAR_RATIO,
  161. BILLBOARD_FAR_DISTANCE,
  162. BILLBOARD_FAR_RATIO,
  163. );
  164. billboard.verticalOrigin = new ConstantProperty(VerticalOrigin.BOTTOM);
  165. billboard.image = image;
  166. return billboard;
  167. }
  168. function createDefaultLabel() {
  169. const label = new LabelGraphics();
  170. label.translucencyByDistance = new NearFarScalar(3000000, 1.0, 5000000, 0.0);
  171. label.pixelOffset = new Cartesian2(17, 0);
  172. label.horizontalOrigin = HorizontalOrigin.LEFT;
  173. label.font = "16px sans-serif";
  174. label.style = LabelStyle.FILL_AND_OUTLINE;
  175. return label;
  176. }
  177. function createDefaultPolyline(color) {
  178. const polyline = new PolylineGraphics();
  179. polyline.width = 4;
  180. polyline.material = new PolylineOutlineMaterialProperty();
  181. polyline.material.color = defined(color) ? color : Color.RED;
  182. polyline.material.outlineWidth = 2;
  183. polyline.material.outlineColor = Color.BLACK;
  184. return polyline;
  185. }
  186. // This is a list of the Optional Description Information:
  187. // <cmt> GPS comment of the waypoint
  188. // <desc> Descriptive description of the waypoint
  189. // <src> Source of the waypoint data
  190. // <type> Type (category) of waypoint
  191. const descriptiveInfoTypes = {
  192. time: {
  193. text: "Time",
  194. tag: "time",
  195. },
  196. comment: {
  197. text: "Comment",
  198. tag: "cmt",
  199. },
  200. description: {
  201. text: "Description",
  202. tag: "desc",
  203. },
  204. source: {
  205. text: "Source",
  206. tag: "src",
  207. },
  208. number: {
  209. text: "GPS track/route number",
  210. tag: "number",
  211. },
  212. type: {
  213. text: "Type",
  214. tag: "type",
  215. },
  216. };
  217. let scratchDiv;
  218. if (typeof document !== "undefined") {
  219. scratchDiv = document.createElement("div");
  220. }
  221. function processDescription(node, entity) {
  222. let i;
  223. let text = "";
  224. const infoTypeNames = Object.keys(descriptiveInfoTypes);
  225. const length = infoTypeNames.length;
  226. for (i = 0; i < length; i++) {
  227. const infoTypeName = infoTypeNames[i];
  228. const infoType = descriptiveInfoTypes[infoTypeName];
  229. infoType.value = queryStringValue(node, infoType.tag, namespaces.gpx) ?? "";
  230. if (defined(infoType.value) && infoType.value !== "") {
  231. text = `${text}<p>${infoType.text}: ${infoType.value}</p>`;
  232. }
  233. }
  234. if (!defined(text) || text === "") {
  235. // No description
  236. return;
  237. }
  238. // Turns non-explicit links into clickable links.
  239. text = autolinker.link(text);
  240. // Use a temporary div to manipulate the links
  241. // so that they open in a new window.
  242. scratchDiv.innerHTML = text;
  243. const links = scratchDiv.querySelectorAll("a");
  244. for (i = 0; i < links.length; i++) {
  245. links[i].setAttribute("target", "_blank");
  246. }
  247. const background = Color.WHITE;
  248. const foreground = Color.BLACK;
  249. let tmp = '<div class="cesium-infoBox-description-lighter" style="';
  250. tmp += "overflow:auto;";
  251. tmp += "word-wrap:break-word;";
  252. tmp += `background-color:${background.toCssColorString()};`;
  253. tmp += `color:${foreground.toCssColorString()};`;
  254. tmp += '">';
  255. tmp += `${scratchDiv.innerHTML}</div>`;
  256. scratchDiv.innerHTML = "";
  257. // return the final HTML as the description.
  258. return tmp;
  259. }
  260. function processWpt(dataSource, geometryNode, entityCollection, options) {
  261. const position = readCoordinateFromNode(geometryNode);
  262. const entity = getOrCreateEntity(geometryNode, entityCollection);
  263. entity.position = position;
  264. // Get billboard image
  265. const image = defined(options.waypointImage)
  266. ? options.waypointImage
  267. : dataSource._pinBuilder.fromMakiIconId(
  268. "marker",
  269. Color.RED,
  270. BILLBOARD_SIZE,
  271. );
  272. entity.billboard = createDefaultBillboard(image);
  273. const name = queryStringValue(geometryNode, "name", namespaces.gpx);
  274. entity.name = name;
  275. entity.label = createDefaultLabel();
  276. entity.label.text = name;
  277. entity.description = processDescription(geometryNode, entity);
  278. if (options.clampToGround) {
  279. entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
  280. entity.label.heightReference = HeightReference.CLAMP_TO_GROUND;
  281. }
  282. }
  283. // rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination
  284. function processRte(dataSource, geometryNode, entityCollection, options) {
  285. const entity = getOrCreateEntity(geometryNode, entityCollection);
  286. entity.description = processDescription(geometryNode, entity);
  287. // a list of waypoint
  288. const routePoints = queryNodes(geometryNode, "rtept", namespaces.gpx);
  289. const coordinateTuples = new Array(routePoints.length);
  290. for (let i = 0; i < routePoints.length; i++) {
  291. processWpt(dataSource, routePoints[i], entityCollection, options);
  292. coordinateTuples[i] = readCoordinateFromNode(routePoints[i]);
  293. }
  294. entity.polyline = createDefaultPolyline(options.routeColor);
  295. if (options.clampToGround) {
  296. entity.polyline.clampToGround = true;
  297. }
  298. entity.polyline.positions = coordinateTuples;
  299. }
  300. // trk represents a track - an ordered list of points describing a path.
  301. function processTrk(dataSource, geometryNode, entityCollection, options) {
  302. const entity = getOrCreateEntity(geometryNode, entityCollection);
  303. entity.description = processDescription(geometryNode, entity);
  304. const trackSegs = queryNodes(geometryNode, "trkseg", namespaces.gpx);
  305. let positions = [];
  306. let times = [];
  307. let trackSegInfo;
  308. let isTimeDynamic = true;
  309. const property = new SampledPositionProperty();
  310. for (let i = 0; i < trackSegs.length; i++) {
  311. trackSegInfo = processTrkSeg(trackSegs[i]);
  312. positions = positions.concat(trackSegInfo.positions);
  313. if (trackSegInfo.times.length > 0) {
  314. times = times.concat(trackSegInfo.times);
  315. property.addSamples(times, positions);
  316. // if one track segment is non dynamic the whole track must also be
  317. isTimeDynamic = isTimeDynamic && true;
  318. } else {
  319. isTimeDynamic = false;
  320. }
  321. }
  322. if (isTimeDynamic) {
  323. // Assign billboard image
  324. const image = defined(options.waypointImage)
  325. ? options.waypointImage
  326. : dataSource._pinBuilder.fromMakiIconId(
  327. "marker",
  328. Color.RED,
  329. BILLBOARD_SIZE,
  330. );
  331. entity.billboard = createDefaultBillboard(image);
  332. entity.position = property;
  333. if (options.clampToGround) {
  334. entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
  335. }
  336. entity.availability = new TimeIntervalCollection();
  337. entity.availability.addInterval(
  338. new TimeInterval({
  339. start: times[0],
  340. stop: times[times.length - 1],
  341. }),
  342. );
  343. }
  344. entity.polyline = createDefaultPolyline(options.trackColor);
  345. entity.polyline.positions = positions;
  346. if (options.clampToGround) {
  347. entity.polyline.clampToGround = true;
  348. }
  349. }
  350. function processTrkSeg(node) {
  351. const result = {
  352. positions: [],
  353. times: [],
  354. };
  355. const trackPoints = queryNodes(node, "trkpt", namespaces.gpx);
  356. let time;
  357. for (let i = 0; i < trackPoints.length; i++) {
  358. const position = readCoordinateFromNode(trackPoints[i]);
  359. result.positions.push(position);
  360. time = queryStringValue(trackPoints[i], "time", namespaces.gpx);
  361. if (defined(time)) {
  362. result.times.push(JulianDate.fromIso8601(time));
  363. }
  364. }
  365. return result;
  366. }
  367. // Processes a metadataType node and returns a metadata object
  368. // {@link http://www.topografix.com/gpx/1/1/#type_metadataType|GPX Schema}
  369. function processMetadata(node) {
  370. const metadataNode = queryFirstNode(node, "metadata", namespaces.gpx);
  371. if (defined(metadataNode)) {
  372. const metadata = {
  373. name: queryStringValue(metadataNode, "name", namespaces.gpx),
  374. desc: queryStringValue(metadataNode, "desc", namespaces.gpx),
  375. author: getPerson(metadataNode),
  376. copyright: getCopyright(metadataNode),
  377. link: getLink(metadataNode),
  378. time: queryStringValue(metadataNode, "time", namespaces.gpx),
  379. keywords: queryStringValue(metadataNode, "keywords", namespaces.gpx),
  380. bounds: getBounds(metadataNode),
  381. };
  382. if (
  383. defined(metadata.name) ||
  384. defined(metadata.desc) ||
  385. defined(metadata.author) ||
  386. defined(metadata.copyright) ||
  387. defined(metadata.link) ||
  388. defined(metadata.time) ||
  389. defined(metadata.keywords) ||
  390. defined(metadata.bounds)
  391. ) {
  392. return metadata;
  393. }
  394. }
  395. return undefined;
  396. }
  397. // Receives a XML node and returns a personType object, refer to
  398. // {@link http://www.topografix.com/gpx/1/1/#type_personType|GPX Schema}
  399. function getPerson(node) {
  400. const personNode = queryFirstNode(node, "author", namespaces.gpx);
  401. if (defined(personNode)) {
  402. const person = {
  403. name: queryStringValue(personNode, "name", namespaces.gpx),
  404. email: getEmail(personNode),
  405. link: getLink(personNode),
  406. };
  407. if (defined(person.name) || defined(person.email) || defined(person.link)) {
  408. return person;
  409. }
  410. }
  411. return undefined;
  412. }
  413. // Receives a XML node and returns an email address (from emailType), refer to
  414. // {@link http://www.topografix.com/gpx/1/1/#type_emailType|GPX Schema}
  415. function getEmail(node) {
  416. const emailNode = queryFirstNode(node, "email", namespaces.gpx);
  417. if (defined(emailNode)) {
  418. const id = queryStringValue(emailNode, "id", namespaces.gpx);
  419. const domain = queryStringValue(emailNode, "domain", namespaces.gpx);
  420. return `${id}@${domain}`;
  421. }
  422. return undefined;
  423. }
  424. // Receives a XML node and returns a linkType object, refer to
  425. // {@link http://www.topografix.com/gpx/1/1/#type_linkType|GPX Schema}
  426. function getLink(node) {
  427. const linkNode = queryFirstNode(node, "link", namespaces.gpx);
  428. if (defined(linkNode)) {
  429. const link = {
  430. href: queryStringAttribute(linkNode, "href"),
  431. text: queryStringValue(linkNode, "text", namespaces.gpx),
  432. mimeType: queryStringValue(linkNode, "type", namespaces.gpx),
  433. };
  434. if (defined(link.href) || defined(link.text) || defined(link.mimeType)) {
  435. return link;
  436. }
  437. }
  438. return undefined;
  439. }
  440. // Receives a XML node and returns a copyrightType object, refer to
  441. // {@link http://www.topografix.com/gpx/1/1/#type_copyrightType|GPX Schema}
  442. function getCopyright(node) {
  443. const copyrightNode = queryFirstNode(node, "copyright", namespaces.gpx);
  444. if (defined(copyrightNode)) {
  445. const copyright = {
  446. author: queryStringAttribute(copyrightNode, "author"),
  447. year: queryStringValue(copyrightNode, "year", namespaces.gpx),
  448. license: queryStringValue(copyrightNode, "license", namespaces.gpx),
  449. };
  450. if (
  451. defined(copyright.author) ||
  452. defined(copyright.year) ||
  453. defined(copyright.license)
  454. ) {
  455. return copyright;
  456. }
  457. }
  458. return undefined;
  459. }
  460. // Receives a XML node and returns a boundsType object, refer to
  461. // {@link http://www.topografix.com/gpx/1/1/#type_boundsType|GPX Schema}
  462. function getBounds(node) {
  463. const boundsNode = queryFirstNode(node, "bounds", namespaces.gpx);
  464. if (defined(boundsNode)) {
  465. const bounds = {
  466. minLat: queryNumericValue(boundsNode, "minlat", namespaces.gpx),
  467. maxLat: queryNumericValue(boundsNode, "maxlat", namespaces.gpx),
  468. minLon: queryNumericValue(boundsNode, "minlon", namespaces.gpx),
  469. maxLon: queryNumericValue(boundsNode, "maxlon", namespaces.gpx),
  470. };
  471. if (
  472. defined(bounds.minLat) ||
  473. defined(bounds.maxLat) ||
  474. defined(bounds.minLon) ||
  475. defined(bounds.maxLon)
  476. ) {
  477. return bounds;
  478. }
  479. }
  480. return undefined;
  481. }
  482. const complexTypes = {
  483. wpt: processWpt,
  484. rte: processRte,
  485. trk: processTrk,
  486. };
  487. function processGpx(dataSource, node, entityCollection, options) {
  488. const complexTypeNames = Object.keys(complexTypes);
  489. const complexTypeNamesLength = complexTypeNames.length;
  490. for (let i = 0; i < complexTypeNamesLength; i++) {
  491. const typeName = complexTypeNames[i];
  492. const processComplexTypeNode = complexTypes[typeName];
  493. const childNodes = node.childNodes;
  494. const length = childNodes.length;
  495. for (let q = 0; q < length; q++) {
  496. const child = childNodes[q];
  497. if (
  498. child.localName === typeName &&
  499. namespaces.gpx.indexOf(child.namespaceURI) !== -1
  500. ) {
  501. processComplexTypeNode(dataSource, child, entityCollection, options);
  502. }
  503. }
  504. }
  505. }
  506. function loadGpx(dataSource, gpx, options) {
  507. const entityCollection = dataSource._entityCollection;
  508. entityCollection.removeAll();
  509. const element = gpx.documentElement;
  510. const version = queryStringAttribute(element, "version");
  511. const creator = queryStringAttribute(element, "creator");
  512. let name;
  513. const metadata = processMetadata(element);
  514. if (defined(metadata)) {
  515. name = metadata.name;
  516. }
  517. if (element.localName === "gpx") {
  518. processGpx(dataSource, element, entityCollection, options);
  519. } else {
  520. console.log(`GPX - Unsupported node: ${element.localName}`);
  521. }
  522. let clock;
  523. const availability = entityCollection.computeAvailability();
  524. let start = availability.start;
  525. let stop = availability.stop;
  526. const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
  527. const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
  528. if (!isMinStart || !isMaxStop) {
  529. let date;
  530. // If start is min time just start at midnight this morning, local time
  531. if (isMinStart) {
  532. date = new Date();
  533. date.setHours(0, 0, 0, 0);
  534. start = JulianDate.fromDate(date);
  535. }
  536. // If stop is max value just stop at midnight tonight, local time
  537. if (isMaxStop) {
  538. date = new Date();
  539. date.setHours(24, 0, 0, 0);
  540. stop = JulianDate.fromDate(date);
  541. }
  542. clock = new DataSourceClock();
  543. clock.startTime = start;
  544. clock.stopTime = stop;
  545. clock.currentTime = JulianDate.clone(start);
  546. clock.clockRange = ClockRange.LOOP_STOP;
  547. clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  548. clock.multiplier = Math.round(
  549. Math.min(
  550. Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
  551. 3.15569e7,
  552. ),
  553. );
  554. }
  555. let changed = false;
  556. if (dataSource._name !== name) {
  557. dataSource._name = name;
  558. changed = true;
  559. }
  560. if (dataSource._creator !== creator) {
  561. dataSource._creator = creator;
  562. changed = true;
  563. }
  564. if (metadataChanged(dataSource._metadata, metadata)) {
  565. dataSource._metadata = metadata;
  566. changed = true;
  567. }
  568. if (dataSource._version !== version) {
  569. dataSource._version = version;
  570. changed = true;
  571. }
  572. if (clock !== dataSource._clock) {
  573. changed = true;
  574. dataSource._clock = clock;
  575. }
  576. if (changed) {
  577. dataSource._changed.raiseEvent(dataSource);
  578. }
  579. DataSource.setLoading(dataSource, false);
  580. return dataSource;
  581. }
  582. function metadataChanged(old, current) {
  583. if (!defined(old) && !defined(current)) {
  584. return false;
  585. } else if (defined(old) && defined(current)) {
  586. if (
  587. old.name !== current.name ||
  588. old.dec !== current.desc ||
  589. old.src !== current.src ||
  590. old.author !== current.author ||
  591. old.copyright !== current.copyright ||
  592. old.link !== current.link ||
  593. old.time !== current.time ||
  594. old.bounds !== current.bounds
  595. ) {
  596. return true;
  597. }
  598. return false;
  599. }
  600. return true;
  601. }
  602. function load(dataSource, entityCollection, data, options) {
  603. options = options ?? Frozen.EMPTY_OBJECT;
  604. let promise = data;
  605. if (typeof data === "string" || data instanceof Resource) {
  606. data = Resource.createIfNeeded(data);
  607. promise = data.fetchBlob();
  608. // Add resource credits to our list of credits to display
  609. const resourceCredits = dataSource._resourceCredits;
  610. const credits = data.credits;
  611. if (defined(credits)) {
  612. const length = credits.length;
  613. for (let i = 0; i < length; i++) {
  614. resourceCredits.push(credits[i]);
  615. }
  616. }
  617. }
  618. return Promise.resolve(promise)
  619. .then(function (dataToLoad) {
  620. if (dataToLoad instanceof Blob) {
  621. return readBlobAsText(dataToLoad).then(function (text) {
  622. // There's no official way to validate if a parse was successful.
  623. // The following check detects the error on various browsers.
  624. // IE raises an exception
  625. let gpx;
  626. let error;
  627. try {
  628. gpx = parser.parseFromString(text, "application/xml");
  629. } catch (e) {
  630. error = e.toString();
  631. }
  632. // The parse succeeds on Chrome and Firefox, but the error
  633. // handling is different in each.
  634. if (
  635. defined(error) ||
  636. gpx.body ||
  637. gpx.documentElement.tagName === "parsererror"
  638. ) {
  639. // Firefox has error information as the firstChild nodeValue.
  640. let msg = defined(error)
  641. ? error
  642. : gpx.documentElement.firstChild.nodeValue;
  643. // Chrome has it in the body text.
  644. if (!msg) {
  645. msg = gpx.body.innerText;
  646. }
  647. // Return the error
  648. throw new RuntimeError(msg);
  649. }
  650. return loadGpx(dataSource, gpx, options);
  651. });
  652. }
  653. return loadGpx(dataSource, dataToLoad, options);
  654. })
  655. .catch(function (error) {
  656. dataSource._error.raiseEvent(dataSource, error);
  657. console.log(error);
  658. return Promise.reject(error);
  659. });
  660. }
  661. /**
  662. * A {@link DataSource} which processes the GPS Exchange Format (GPX).
  663. *
  664. * @alias GpxDataSource
  665. * @constructor
  666. *
  667. * @see {@link http://www.topografix.com/gpx.asp|Topografix GPX Standard}
  668. * @see {@link http://www.topografix.com/gpx/1/1/|Topografix GPX Documentation}
  669. *
  670. * @demo {@link http://sandcastle.cesium.com/index.html?id=gpx}
  671. *
  672. * @example
  673. * const viewer = new Cesium.Viewer('cesiumContainer');
  674. * viewer.dataSources.add(Cesium.GpxDataSource.load('../../SampleData/track.gpx'));
  675. */
  676. function GpxDataSource() {
  677. this._changed = new Event();
  678. this._error = new Event();
  679. this._loading = new Event();
  680. this._clock = undefined;
  681. this._entityCollection = new EntityCollection(this);
  682. this._entityCluster = new EntityCluster();
  683. this._name = undefined;
  684. this._version = undefined;
  685. this._creator = undefined;
  686. this._metadata = undefined;
  687. this._isLoading = false;
  688. this._pinBuilder = new PinBuilder();
  689. }
  690. /**
  691. * Creates a Promise to a new instance loaded with the provided GPX data.
  692. *
  693. * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data.
  694. * @param {object} [options] An object with the following properties:
  695. * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
  696. * @param {string} [options.waypointImage] Image to use for waypoint billboards.
  697. * @param {string} [options.trackImage] Image to use for track billboards.
  698. * @param {string} [options.trackColor] Color to use for track lines.
  699. * @param {string} [options.routeColor] Color to use for route lines.
  700. * @returns {Promise<GpxDataSource>} A promise that will resolve to a new GpxDataSource instance once the gpx is loaded.
  701. */
  702. GpxDataSource.load = function (data, options) {
  703. return new GpxDataSource().load(data, options);
  704. };
  705. Object.defineProperties(GpxDataSource.prototype, {
  706. /**
  707. * Gets a human-readable name for this instance.
  708. * This will be automatically be set to the GPX document name on load.
  709. * @memberof GpxDataSource.prototype
  710. * @type {string}
  711. */
  712. name: {
  713. get: function () {
  714. return this._name;
  715. },
  716. },
  717. /**
  718. * Gets the version of the GPX Schema in use.
  719. * @memberof GpxDataSource.prototype
  720. * @type {string}
  721. */
  722. version: {
  723. get: function () {
  724. return this._version;
  725. },
  726. },
  727. /**
  728. * Gets the creator of the GPX document.
  729. * @memberof GpxDataSource.prototype
  730. * @type {string}
  731. */
  732. creator: {
  733. get: function () {
  734. return this._creator;
  735. },
  736. },
  737. /**
  738. * Gets an object containing metadata about the GPX file.
  739. * @memberof GpxDataSource.prototype
  740. * @type {object}
  741. */
  742. metadata: {
  743. get: function () {
  744. return this._metadata;
  745. },
  746. },
  747. /**
  748. * Gets the clock settings defined by the loaded GPX. This represents the total
  749. * availability interval for all time-dynamic data. If the GPX does not contain
  750. * time-dynamic data, this value is undefined.
  751. * @memberof GpxDataSource.prototype
  752. * @type {DataSourceClock}
  753. */
  754. clock: {
  755. get: function () {
  756. return this._clock;
  757. },
  758. },
  759. /**
  760. * Gets the collection of {@link Entity} instances.
  761. * @memberof GpxDataSource.prototype
  762. * @type {EntityCollection}
  763. */
  764. entities: {
  765. get: function () {
  766. return this._entityCollection;
  767. },
  768. },
  769. /**
  770. * Gets a value indicating if the data source is currently loading data.
  771. * @memberof GpxDataSource.prototype
  772. * @type {boolean}
  773. */
  774. isLoading: {
  775. get: function () {
  776. return this._isLoading;
  777. },
  778. },
  779. /**
  780. * Gets an event that will be raised when the underlying data changes.
  781. * @memberof GpxDataSource.prototype
  782. * @type {Event}
  783. */
  784. changedEvent: {
  785. get: function () {
  786. return this._changed;
  787. },
  788. },
  789. /**
  790. * Gets an event that will be raised if an error is encountered during processing.
  791. * @memberof GpxDataSource.prototype
  792. * @type {Event}
  793. */
  794. errorEvent: {
  795. get: function () {
  796. return this._error;
  797. },
  798. },
  799. /**
  800. * Gets an event that will be raised when the data source either starts or stops loading.
  801. * @memberof GpxDataSource.prototype
  802. * @type {Event}
  803. */
  804. loadingEvent: {
  805. get: function () {
  806. return this._loading;
  807. },
  808. },
  809. /**
  810. * Gets whether or not this data source should be displayed.
  811. * @memberof GpxDataSource.prototype
  812. * @type {boolean}
  813. */
  814. show: {
  815. get: function () {
  816. return this._entityCollection.show;
  817. },
  818. set: function (value) {
  819. this._entityCollection.show = value;
  820. },
  821. },
  822. /**
  823. * Gets or sets the clustering options for this data source. This object can be shared between multiple data sources.
  824. *
  825. * @memberof GpxDataSource.prototype
  826. * @type {EntityCluster}
  827. */
  828. clustering: {
  829. get: function () {
  830. return this._entityCluster;
  831. },
  832. set: function (value) {
  833. //>>includeStart('debug', pragmas.debug);
  834. if (!defined(value)) {
  835. throw new DeveloperError("value must be defined.");
  836. }
  837. //>>includeEnd('debug');
  838. this._entityCluster = value;
  839. },
  840. },
  841. });
  842. /**
  843. * Updates the data source to the provided time. This function is optional and
  844. * is not required to be implemented. It is provided for data sources which
  845. * retrieve data based on the current animation time or scene state.
  846. * If implemented, update will be called by {@link DataSourceDisplay} once a frame.
  847. *
  848. * @param {JulianDate} time The simulation time.
  849. * @returns {boolean} True if this data source is ready to be displayed at the provided time, false otherwise.
  850. */
  851. GpxDataSource.prototype.update = function (time) {
  852. return true;
  853. };
  854. /**
  855. * Asynchronously loads the provided GPX data, replacing any existing data.
  856. *
  857. * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data or a parsed GPX document.
  858. * @param {object} [options] An object with the following properties:
  859. * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
  860. * @param {string} [options.waypointImage] Image to use for waypoint billboards.
  861. * @param {string} [options.trackImage] Image to use for track billboards.
  862. * @param {string} [options.trackColor] Color to use for track lines.
  863. * @param {string} [options.routeColor] Color to use for route lines.
  864. * @returns {Promise<GpxDataSource>} A promise that will resolve to this instances once the GPX is loaded.
  865. */
  866. GpxDataSource.prototype.load = function (data, options) {
  867. if (!defined(data)) {
  868. throw new DeveloperError("data is required.");
  869. }
  870. options = options ?? Frozen.EMPTY_OBJECT;
  871. DataSource.setLoading(this, true);
  872. const oldName = this._name;
  873. const that = this;
  874. return load(this, this._entityCollection, data, options)
  875. .then(function () {
  876. let clock;
  877. const availability = that._entityCollection.computeAvailability();
  878. let start = availability.start;
  879. let stop = availability.stop;
  880. const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
  881. const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
  882. if (!isMinStart || !isMaxStop) {
  883. let date;
  884. // If start is min time just start at midnight this morning, local time
  885. if (isMinStart) {
  886. date = new Date();
  887. date.setHours(0, 0, 0, 0);
  888. start = JulianDate.fromDate(date);
  889. }
  890. // If stop is max value just stop at midnight tonight, local time
  891. if (isMaxStop) {
  892. date = new Date();
  893. date.setHours(24, 0, 0, 0);
  894. stop = JulianDate.fromDate(date);
  895. }
  896. clock = new DataSourceClock();
  897. clock.startTime = start;
  898. clock.stopTime = stop;
  899. clock.currentTime = JulianDate.clone(start);
  900. clock.clockRange = ClockRange.LOOP_STOP;
  901. clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  902. clock.multiplier = Math.round(
  903. Math.min(
  904. Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
  905. 3.15569e7,
  906. ),
  907. );
  908. }
  909. let changed = false;
  910. if (clock !== that._clock) {
  911. that._clock = clock;
  912. changed = true;
  913. }
  914. if (oldName !== that._name) {
  915. changed = true;
  916. }
  917. if (changed) {
  918. that._changed.raiseEvent(that);
  919. }
  920. DataSource.setLoading(that, false);
  921. return that;
  922. })
  923. .catch(function (error) {
  924. DataSource.setLoading(that, false);
  925. that._error.raiseEvent(that, error);
  926. console.log(error);
  927. return Promise.reject(error);
  928. });
  929. };
  930. export default GpxDataSource;