GpxDataSource.js 30 KB

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